diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index c9ee47e0b6..53e835c198 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -3,6 +3,7 @@ "allow_import": 0, "allow_rename": 1, "autoname": "Prompt", + "beta": 0, "creation": "2013-02-18 13:36:19", "custom": 0, "description": "DocType is a Table / Form in the application.", @@ -239,6 +240,32 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval: doc.image_field", + "fieldname": "image_view", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Image View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -1097,7 +1124,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-05-02 18:02:03.227684", + "modified": "2016-06-01 12:28:01.048158", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 5bba69490c..38f49498f3 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -3,6 +3,7 @@ "allow_import": 0, "allow_rename": 0, "autoname": "DL.####", + "beta": 0, "creation": "2013-01-29 17:55:08", "custom": 0, "docstatus": 0, @@ -159,6 +160,32 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "depends_on": "eval: doc.image_field", + "fieldname": "image_view", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Image View", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -414,13 +441,14 @@ "hide_toolbar": 1, "icon": "icon-glass", "idx": 1, + "image_view": 0, "in_create": 0, "in_dialog": 0, "is_submittable": 0, "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2016-04-14 13:18:35.095815", + "modified": "2016-06-01 12:29:28.272410", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 1f0db5f6b7..3639251b3f 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -23,7 +23,8 @@ doctype_properties = { 'read_only_onload': 'Check', 'allow_copy': 'Check', 'quick_entry': 'Check', - 'max_attachments': 'Int' + 'max_attachments': 'Int', + 'image_view': 'Check' } docfield_properties = { diff --git a/frappe/public/build.json b/frappe/public/build.json index e01f97b8d4..6c48d16691 100644 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -218,7 +218,13 @@ "public/js/frappe/list/listview.js", "public/js/frappe/views/calendar_base.js", "public/js/frappe/views/calendar.js", - "public/js/frappe/views/ganttview.js" + "public/js/frappe/views/ganttview.js", + + "public/js/frappe/list/blueimp-gallery.html", + "public/js/frappe/list/image_view_item_row.html", + "public/js/frappe/list/image_view_item_main_head.html", + "public/js/frappe/list/header_select_all_like_filter.html", + "public/js/frappe/list/item_assigned_to_comment_count.html" ], "css/report.min.css": [ "public/css/report.css", @@ -248,4 +254,4 @@ "public/js/legacy/print_table.js", "public/js/legacy/print_format.js" ] -} +} \ No newline at end of file diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 2f6110386f..072726590d 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -189,3 +189,34 @@ width: 37px; text-align: left; } + +.image-view { + float: left; +} + +.image-view-subject { + padding: inherit; +} + +.image-view-col { + padding-bottom: 5px; + padding-top: 5px; +} + +table.field-info { + opacity: 0; + bottom: -20px; + font-size: 8pt; + color: white; + background-color: #000000; + position: absolute; + padding-bottom: 0px !important; +} + +.image-field { + position: relative; +} + +.image-field:hover .field-info { + opacity: 0.9; +} \ No newline at end of file diff --git a/frappe/public/js/frappe/list/blueimp-gallery.html b/frappe/public/js/frappe/list/blueimp-gallery.html new file mode 100644 index 0000000000..9503e2cce9 --- /dev/null +++ b/frappe/public/js/frappe/list/blueimp-gallery.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/frappe/public/js/frappe/list/doclistview.js b/frappe/public/js/frappe/list/doclistview.js index bf539a83fb..10a93b1573 100644 --- a/frappe/public/js/frappe/list/doclistview.js +++ b/frappe/public/js/frappe/list/doclistview.js @@ -133,7 +133,8 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ }, init_headers: function() { - var main = frappe.render_template("list_item_main_head", { + this.header = this.meta.image_view == 0? "list_item_main_head": "image_view_item_main_head"; + var main = frappe.render_template(this.header, { columns: this.listview.columns, right_column: this.listview.settings.right_column, _checkbox: ((frappe.model.can_delete(this.doctype) || this.listview.settings.selectable) @@ -445,6 +446,12 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ data.doctype = this.doctype; this.listview.render(row, data, this); }, + render_image_view_row: function(row, data) { + for (var i = 0; i < data.length; i++) { + data[i].doctype = this.doctype; + this.listview.render(row, data[i], this) + } + }, get_args: function() { var args = { doctype: this.doctype, @@ -707,8 +714,14 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ }, get_checked_items: function() { + var me = this; return $.map(this.$page.find('.list-delete:checked'), function(e) { - return $(e).parents(".list-row:first").data('data'); + if(me.meta.image_view == 0){ + return $(e).parents(".list-row:first").data('data'); + } + else{ + return $(e).parents(".image-view:first").data('data'); + } }); }, diff --git a/frappe/public/js/frappe/list/header_select_all_like_filter.html b/frappe/public/js/frappe/list/header_select_all_like_filter.html new file mode 100644 index 0000000000..57bee297d1 --- /dev/null +++ b/frappe/public/js/frappe/list/header_select_all_like_filter.html @@ -0,0 +1,7 @@ +{% if (_checkbox) { %} + +{% } %} + + \ No newline at end of file diff --git a/frappe/public/js/frappe/list/image_view_item_main_head.html b/frappe/public/js/frappe/list/image_view_item_main_head.html new file mode 100644 index 0000000000..1b2cf4aa7d --- /dev/null +++ b/frappe/public/js/frappe/list/image_view_item_main_head.html @@ -0,0 +1,5 @@ +
+
+ {%= frappe.render_template("header_select_all_like_filter", { _checkbox: _checkbox }) %} +
+
\ No newline at end of file diff --git a/frappe/public/js/frappe/list/image_view_item_row.html b/frappe/public/js/frappe/list/image_view_item_row.html new file mode 100644 index 0000000000..bd022999b8 --- /dev/null +++ b/frappe/public/js/frappe/list/image_view_item_row.html @@ -0,0 +1,39 @@ +
+
+
+ {%= subject %} +
+
+ +
+
+
+ {% if (!item_image) { %}{%= frappe.get_abbr(data.name) %}{% } %} + + {% for (var i=0, l=columns.length; i < l; i++ ) { + var col = columns[i], value=data[col.fieldname]; %} + {% if(in_list(allowed_type, col.fieldtype)) { %} + + + + + {% } %} + {% } %} +
{%= col.title %}{%= value %}
+
+
+
+
+ +
+
+
{%= list.get_indicator(data) %}
+
+ + {%= frappe.render_template("item_assigned_to_comment_count", { data: data }) %} +
+
+
+
+
diff --git a/frappe/public/js/frappe/list/imageview.js b/frappe/public/js/frappe/list/imageview.js new file mode 100644 index 0000000000..c8c7f7883a --- /dev/null +++ b/frappe/public/js/frappe/list/imageview.js @@ -0,0 +1,61 @@ +frappe.views.ImageView = Class.extend({ + init: function(opts){ + this.doctype = opts.doctype; + this.docname = opts.docname; + this.container = opts.container; + + this.get_images(this.doctype, this.docname); + }, + + get_images: function(doctype, docname){ + /* get the list of all the Images associated with doc */ + var me = this; + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "File", + fields: [ + "file_name as title", "file_url as href", + "'image/*' as type", "ifnull(thumbnail_url, file_url) as thumbnail" + ], + filters: [ + ["File", "attached_to_doctype", "=", this.doctype], + ["File", "attached_to_name", "=", this.docname], + ["File", "is_folder", "!=", 1] + ] + }, + freeze: true, + freeze_message: "Fetching Images ..", + callback: function(r){ + if(!r.message){ + msgprint("No Images found") + } else{ + // filter image files from other + images = r.message.filter(function(image){ + return frappe.utils.is_image_file(image.title); + }); + + if(images){ + me.render(images); + } + } + } + }); + }, + + render: function(image_list){ + var gallery = blueimp.Gallery(image_list, this.get_options()); + }, + + get_options: function(){ + /* options for the gallery plugin */ + return { + indicatorContainer: 'ol', + thumbnailIndicators: true, + thumbnailProperty: 'thumbnail', + activeIndicatorClass: 'active', + container: this.container.find(".blueimp-gallery") + } + } +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/list/item_assigned_to_comment_count.html b/frappe/public/js/frappe/list/item_assigned_to_comment_count.html new file mode 100644 index 0000000000..676fab1457 --- /dev/null +++ b/frappe/public/js/frappe/list/item_assigned_to_comment_count.html @@ -0,0 +1,17 @@ + diff --git a/frappe/public/js/frappe/list/list_item_main_head.html b/frappe/public/js/frappe/list/list_item_main_head.html index 765551c4c1..1a15a8861f 100644 --- a/frappe/public/js/frappe/list/list_item_main_head.html +++ b/frappe/public/js/frappe/list/list_item_main_head.html @@ -17,15 +17,7 @@ {% if(col.df && ["Int", "Float", "Currency", "Percent"].indexOf(col.df.fieldtype)!==-1) { %}text-right{% } %}"> {% if (col.type==="Subject") { %} - - {% if (_checkbox) { %} - - {% } %} - - - + {%= frappe.render_template("header_select_all_like_filter", { _checkbox: _checkbox }) %} {% } %} {%= __(col.title) || __(col.label) || "" %} diff --git a/frappe/public/js/frappe/list/list_item_row.html b/frappe/public/js/frappe/list/list_item_row.html index c2afc7b2f9..3c83df4203 100644 --- a/frappe/public/js/frappe/list/list_item_row.html +++ b/frappe/public/js/frappe/list/list_item_row.html @@ -25,22 +25,7 @@
{%= list.get_indicator_dot(data) %}
- + + {%= frappe.render_template("item_assigned_to_comment_count", { data: data }) %}
diff --git a/frappe/public/js/frappe/list/listview.js b/frappe/public/js/frappe/list/listview.js index e4ded1a30a..2ca06dbb4d 100644 --- a/frappe/public/js/frappe/list/listview.js +++ b/frappe/public/js/frappe/list/listview.js @@ -124,7 +124,6 @@ frappe.views.ListView = Class.extend({ this.total_colspans += this.columns[1].colspan; } - // overridden var overridden = $.map(this.settings.add_columns || [], function(d) { return d.content; @@ -203,7 +202,21 @@ frappe.views.ListView = Class.extend({ this.id_list.push(data.name); } + if(this.meta.image_view == 0){ + this.render_list_row(row, data); + } + else{ + this.render_list_image(row, data); + } + + if(this.settings.post_render_item) { + this.settings.post_render_item(this, row, data); + } + + this.render_tags(row, data); + }, + render_list_row: function(row, data) { var main = frappe.render_template("list_item_main", { data: data, columns: this.columns, @@ -218,15 +231,26 @@ frappe.views.ListView = Class.extend({ list: this, right_column: this.settings.right_column })).appendTo(row); - - if(this.settings.post_render_item) { - this.settings.post_render_item(this, row, data); - } - - this.render_tags(row, data); - }, - + render_list_image: function(row, data) { + this.allowed_type = [ + "Check", "Currency", "Data", "Date", + "Datetime", "Float", "Int", "Link", + "Percent", "Select", "Read Only", "Time" + ]; + img_col = $(frappe.render_template("image_view_item_row", { + data: data, + list: this, + columns: this.columns, + allowed_type: this.allowed_type, + item_image: data.image ? "url('" + data.image + "')" : null, + color: frappe.get_palette(data.item_name), + subject: this.get_avatar_and_id(data, true), + right_column: this.settings.right_column + })) + .data("data", data) + .appendTo($(row).find(".image-view-marker")); + }, render_tags: function(row, data) { var me = this; var row2 = $('
\ @@ -366,4 +390,4 @@ frappe.views.ListView = Class.extend({ var icon_html = ""; $(parent).append(repl(icon_html, {icon_class: icon_class, label: __(label) || ''})); } -}); +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/listing.js b/frappe/public/js/frappe/ui/listing.js index b596854595..a91aea5ae1 100644 --- a/frappe/public/js/frappe/ui/listing.js +++ b/frappe/public/js/frappe/ui/listing.js @@ -302,17 +302,58 @@ frappe.ui.Listing = Class.extend({ }, render_list: function(values) { - var m = Math.min(values.length, this.page_length); this.last_page = values; if(this.filter_list) { this.filter_values = this.filter_list.get_filters(); } + this.render_rows(values); + }, + render_rows: function(values) { // render the rows - for(var i=0; i < m; i++) { - this.render_row(this.add_row(values[i]), values[i], this, i); + if(this.meta.image_view == 0){ + var m = Math.min(values.length, this.page_length); + for(var i=0; i < m; i++) { + this.render_row(this.add_row(values[i]), values[i], this, i); + } + } + else { + var cols = values.slice(); + while (cols.length) { + row = this.add_row(cols[0]); + $("
").appendTo(row); + this.render_image_view_row(row, cols.splice(0, 4), this, i); + } + + this.render_image_gallery(); } }, + render_image_gallery: function(){ + var me = this; + frappe.require( + [ + "assets/frappe/js/frappe/list/imageview.js", + "assets/frappe/js/lib/gallery/js/blueimp-gallery.js", + "assets/frappe/js/lib/gallery/css/blueimp-gallery.css", + "assets/frappe/js/lib/gallery/js/blueimp-gallery-indicator.js", + "assets/frappe/js/lib/gallery/css/blueimp-gallery-indicator.css" + ], function(){ + // remove previous gallery container + me.$w.find(".blueimp-gallery").remove(); + // append gallery div + var gallery = frappe.render_template("blueimp-gallery", {}); + $(gallery).appendTo(me.$w); + + me.$w.find(".image-field").click(function(event){ + opts = { + doctype: me.doctype, + docname: $(event.target).attr('data-name'), + container: me.$w + }; + new frappe.views.ImageView(opts); + }); + }); + }, update_paging: function(values) { if(values.length >= this.page_length) { this.$w.find('.list-paging-area').toggle(true); @@ -321,7 +362,7 @@ frappe.ui.Listing = Class.extend({ }, add_row: function(row) { return $('
') - .data("data", row) + .data("data", this.meta.image_view == 0?row:null) .appendTo(this.$w.find('.result-list')) .get(0); }, diff --git a/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css b/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css new file mode 100644 index 0000000000..9bf18b91a5 --- /dev/null +++ b/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css @@ -0,0 +1,72 @@ +@charset "UTF-8"; +/* + * blueimp Gallery Indicator CSS + * https://github.com/blueimp/Gallery + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +.blueimp-gallery > .indicator { + position: absolute; + top: auto; + right: 15px; + bottom: 15px; + left: 15px; + margin: 0 40px; + padding: 0; + list-style: none; + text-align: center; + line-height: 10px; + display: none; +} +.blueimp-gallery > .indicator > li { + display: inline-block; + width: 40px; + height: 40px; + margin: 6px 3px 0 3px; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + border: 1px solid transparent; + background: #ccc; + background: rgba(255, 255, 255, 0.25) center no-repeat; + border-radius: 5px; + box-shadow: 0 0 2px #000; + opacity: 0.5; + cursor: pointer; + background-size: 40px 40px; +} +.blueimp-gallery > .indicator > li:hover, +.blueimp-gallery > .indicator > .active { + background-color: #fff; + border-color: #fff; + opacity: 1; +} +.blueimp-gallery-controls > .indicator { + display: block; + /* Fix z-index issues (controls behind slide element) on Android: */ + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + transform: translateZ(0); +} +.blueimp-gallery-single > .indicator { + display: none; +} +.blueimp-gallery > .indicator { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* IE7 fixes */ +*+html .blueimp-gallery > .indicator > li { + display: inline; +} diff --git a/frappe/public/js/lib/gallery/css/blueimp-gallery.css b/frappe/public/js/lib/gallery/css/blueimp-gallery.css new file mode 100644 index 0000000000..b47ea200b4 --- /dev/null +++ b/frappe/public/js/lib/gallery/css/blueimp-gallery.css @@ -0,0 +1,226 @@ +@charset "UTF-8"; +/* + * blueimp Gallery CSS + * https://github.com/blueimp/Gallery + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +.blueimp-gallery, +.blueimp-gallery > .slides > .slide > .slide-content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + /* Prevent artifacts in Mozilla Firefox: */ + -moz-backface-visibility: hidden; +} +.blueimp-gallery > .slides > .slide > .slide-content { + margin: auto; + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + opacity: 1; +} +.blueimp-gallery { + position: fixed; + z-index: 999999; + overflow: hidden; + background: #000; + background: rgba(0, 0, 0, 0.9); + opacity: 0; + display: none; + direction: ltr; + -ms-touch-action: none; + touch-action: none; +} +.blueimp-gallery-carousel { + position: relative; + z-index: auto; + margin: 1em auto; + /* Set the carousel width/height ratio to 16/9: */ + padding-bottom: 56.25%; + box-shadow: 0 0 10px #000; + -ms-touch-action: pan-y; + touch-action: pan-y; +} +.blueimp-gallery-display { + display: block; + opacity: 1; +} +.blueimp-gallery > .slides { + position: relative; + height: 100%; + overflow: hidden; +} +.blueimp-gallery-carousel > .slides { + position: absolute; +} +.blueimp-gallery > .slides > .slide { + position: relative; + float: left; + height: 100%; + text-align: center; + -webkit-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); + -moz-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); + -ms-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); + -o-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); + transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); +} +.blueimp-gallery, +.blueimp-gallery > .slides > .slide > .slide-content { + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -ms-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} +.blueimp-gallery > .slides > .slide-loading { + background: url(../img/loading.gif) center no-repeat; + background-size: 64px 64px; +} +.blueimp-gallery > .slides > .slide-loading > .slide-content { + opacity: 0; +} +.blueimp-gallery > .slides > .slide-error { + background: url(../img/error.png) center no-repeat; +} +.blueimp-gallery > .slides > .slide-error > .slide-content { + display: none; +} +.blueimp-gallery > .prev, +.blueimp-gallery > .next { + position: absolute; + top: 50%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -23px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #fff; + text-decoration: none; + text-shadow: 0 0 2px #000; + text-align: center; + background: #222; + background: rgba(0, 0, 0, 0.5); + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + border: 3px solid #fff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + cursor: pointer; + display: none; +} +.blueimp-gallery > .next { + left: auto; + right: 15px; +} +.blueimp-gallery > .close, +.blueimp-gallery > .title { + position: absolute; + top: 15px; + left: 15px; + margin: 0 40px 0 0; + font-size: 20px; + line-height: 30px; + color: #fff; + text-shadow: 0 0 2px #000; + opacity: 0.8; + display: none; +} +.blueimp-gallery > .close { + padding: 15px; + right: 15px; + left: auto; + margin: -15px; + font-size: 30px; + text-decoration: none; + cursor: pointer; +} +.blueimp-gallery > .play-pause { + position: absolute; + right: 15px; + bottom: 15px; + width: 15px; + height: 15px; + background: url(../img/play-pause.png) 0 0 no-repeat; + cursor: pointer; + opacity: 0.5; + display: none; +} +.blueimp-gallery-playing > .play-pause { + background-position: -15px 0; +} +.blueimp-gallery > .prev:hover, +.blueimp-gallery > .next:hover, +.blueimp-gallery > .close:hover, +.blueimp-gallery > .title:hover, +.blueimp-gallery > .play-pause:hover { + color: #fff; + opacity: 1; +} +.blueimp-gallery-controls > .prev, +.blueimp-gallery-controls > .next, +.blueimp-gallery-controls > .close, +.blueimp-gallery-controls > .title, +.blueimp-gallery-controls > .play-pause { + display: block; + /* Fix z-index issues (controls behind slide element) on Android: */ + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + transform: translateZ(0); +} +.blueimp-gallery-single > .prev, +.blueimp-gallery-left > .prev, +.blueimp-gallery-single > .next, +.blueimp-gallery-right > .next, +.blueimp-gallery-single > .play-pause { + display: none; +} +.blueimp-gallery > .slides > .slide > .slide-content, +.blueimp-gallery > .prev, +.blueimp-gallery > .next, +.blueimp-gallery > .close, +.blueimp-gallery > .play-pause { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Replace PNGs with SVGs for capable browsers (excluding IE<9) */ +body:last-child .blueimp-gallery > .slides > .slide-error { + background-image: url(../img/error.svg); +} +body:last-child .blueimp-gallery > .play-pause { + width: 20px; + height: 20px; + background-size: 40px 20px; + background-image: url(../img/play-pause.svg); +} +body:last-child .blueimp-gallery-playing > .play-pause { + background-position: -20px 0; +} + +/* IE7 fixes */ +*+html .blueimp-gallery > .slides > .slide { + min-height: 300px; +} +*+html .blueimp-gallery > .slides > .slide > .slide-content { + position: relative; +} diff --git a/frappe/public/js/lib/gallery/js/blueimp-gallery-indicator.js b/frappe/public/js/lib/gallery/js/blueimp-gallery-indicator.js new file mode 100644 index 0000000000..0470f6326a --- /dev/null +++ b/frappe/public/js/lib/gallery/js/blueimp-gallery-indicator.js @@ -0,0 +1,155 @@ +/* + * blueimp Gallery Indicator JS + * https://github.com/blueimp/Gallery + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, window, document */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + './blueimp-helper', + './blueimp-gallery' + ], factory) + } else { + // Browser globals: + factory( + window.blueimp.helper || window.jQuery, + window.blueimp.Gallery + ) + } +}(function ($, Gallery) { + 'use strict' + + $.extend(Gallery.prototype.options, { + // The tag name, Id, element or querySelector of the indicator container: + indicatorContainer: 'ol', + // The class for the active indicator: + activeIndicatorClass: 'active', + // The list object property (or data attribute) with the thumbnail URL, + // used as alternative to a thumbnail child element: + thumbnailProperty: 'thumbnail', + // Defines if the gallery indicators should display a thumbnail: + thumbnailIndicators: true + }) + + var initSlides = Gallery.prototype.initSlides + var addSlide = Gallery.prototype.addSlide + var resetSlides = Gallery.prototype.resetSlides + var handleClick = Gallery.prototype.handleClick + var handleSlide = Gallery.prototype.handleSlide + var handleClose = Gallery.prototype.handleClose + + $.extend(Gallery.prototype, { + createIndicator: function (obj) { + var indicator = this.indicatorPrototype.cloneNode(false) + var title = this.getItemProperty(obj, this.options.titleProperty) + var thumbnailProperty = this.options.thumbnailProperty + var thumbnailUrl + var thumbnail + if (this.options.thumbnailIndicators) { + if (thumbnailProperty) { + thumbnailUrl = this.getItemProperty(obj, thumbnailProperty) + } + if (thumbnailUrl === undefined) { + thumbnail = obj.getElementsByTagName && $(obj).find('img')[0] + if (thumbnail) { + thumbnailUrl = thumbnail.src + } + } + if (thumbnailUrl) { + indicator.style.backgroundImage = 'url("' + thumbnailUrl + '")' + } + } + if (title) { + indicator.title = title + } + return indicator + }, + + addIndicator: function (index) { + if (this.indicatorContainer.length) { + var indicator = this.createIndicator(this.list[index]) + indicator.setAttribute('data-index', index) + this.indicatorContainer[0].appendChild(indicator) + this.indicators.push(indicator) + } + }, + + setActiveIndicator: function (index) { + if (this.indicators) { + if (this.activeIndicator) { + this.activeIndicator + .removeClass(this.options.activeIndicatorClass) + } + this.activeIndicator = $(this.indicators[index]) + this.activeIndicator + .addClass(this.options.activeIndicatorClass) + } + }, + + initSlides: function (reload) { + if (!reload) { + this.indicatorContainer = this.container.find( + this.options.indicatorContainer + ) + if (this.indicatorContainer.length) { + this.indicatorPrototype = document.createElement('li') + this.indicators = this.indicatorContainer[0].children + } + } + initSlides.call(this, reload) + }, + + addSlide: function (index) { + addSlide.call(this, index) + this.addIndicator(index) + }, + + resetSlides: function () { + resetSlides.call(this) + this.indicatorContainer.empty() + this.indicators = [] + }, + + handleClick: function (event) { + var target = event.target || event.srcElement + var parent = target.parentNode + if (parent === this.indicatorContainer[0]) { + // Click on indicator element + this.preventDefault(event) + this.slide(this.getNodeIndex(target)) + } else if (parent.parentNode === this.indicatorContainer[0]) { + // Click on indicator child element + this.preventDefault(event) + this.slide(this.getNodeIndex(parent)) + } else { + return handleClick.call(this, event) + } + }, + + handleSlide: function (index) { + handleSlide.call(this, index) + this.setActiveIndicator(index) + }, + + handleClose: function () { + if (this.activeIndicator) { + this.activeIndicator + .removeClass(this.options.activeIndicatorClass) + } + handleClose.call(this) + } + + }) + + return Gallery +})) diff --git a/frappe/public/js/lib/gallery/js/blueimp-gallery.js b/frappe/public/js/lib/gallery/js/blueimp-gallery.js new file mode 100644 index 0000000000..d7c827d4a9 --- /dev/null +++ b/frappe/public/js/lib/gallery/js/blueimp-gallery.js @@ -0,0 +1,1389 @@ +/* + * blueimp Gallery JS + * https://github.com/blueimp/Gallery + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Swipe implementation based on + * https://github.com/bradbirdsall/Swipe + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, window, document, DocumentTouch */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./blueimp-helper'], factory) + } else { + // Browser globals: + window.blueimp = window.blueimp || {} + window.blueimp.Gallery = factory( + window.blueimp.helper || window.jQuery + ) + } +}(function ($) { + 'use strict' + + function Gallery (list, options) { + if (document.body.style.maxHeight === undefined) { + // document.body.style.maxHeight is undefined on IE6 and lower + return null + } + if (!this || this.options !== Gallery.prototype.options) { + // Called as function instead of as constructor, + // so we simply return a new instance: + return new Gallery(list, options) + } + if (!list || !list.length) { + this.console.log( + 'blueimp Gallery: No or empty list provided as first argument.', + list + ) + return + } + this.list = list + this.num = list.length + this.initOptions(options) + this.initialize() + } + + $.extend(Gallery.prototype, { + options: { + // The Id, element or querySelector of the gallery widget: + container: '#blueimp-gallery', + // The tag name, Id, element or querySelector of the slides container: + slidesContainer: 'div', + // The tag name, Id, element or querySelector of the title element: + titleElement: 'h3', + // The class to add when the gallery is visible: + displayClass: 'blueimp-gallery-display', + // The class to add when the gallery controls are visible: + controlsClass: 'blueimp-gallery-controls', + // The class to add when the gallery only displays one element: + singleClass: 'blueimp-gallery-single', + // The class to add when the left edge has been reached: + leftEdgeClass: 'blueimp-gallery-left', + // The class to add when the right edge has been reached: + rightEdgeClass: 'blueimp-gallery-right', + // The class to add when the automatic slideshow is active: + playingClass: 'blueimp-gallery-playing', + // The class for all slides: + slideClass: 'slide', + // The slide class for loading elements: + slideLoadingClass: 'slide-loading', + // The slide class for elements that failed to load: + slideErrorClass: 'slide-error', + // The class for the content element loaded into each slide: + slideContentClass: 'slide-content', + // The class for the "toggle" control: + toggleClass: 'toggle', + // The class for the "prev" control: + prevClass: 'prev', + // The class for the "next" control: + nextClass: 'next', + // The class for the "close" control: + closeClass: 'close', + // The class for the "play-pause" toggle control: + playPauseClass: 'play-pause', + // The list object property (or data attribute) with the object type: + typeProperty: 'type', + // The list object property (or data attribute) with the object title: + titleProperty: 'title', + // The list object property (or data attribute) with the object URL: + urlProperty: 'href', + // The list object property (or data attribute) with the object srcset URL(s): + srcsetProperty: 'urlset', + // The gallery listens for transitionend events before triggering the + // opened and closed events, unless the following option is set to false: + displayTransition: true, + // Defines if the gallery slides are cleared from the gallery modal, + // or reused for the next gallery initialization: + clearSlides: true, + // Defines if images should be stretched to fill the available space, + // while maintaining their aspect ratio (will only be enabled for browsers + // supporting background-size="contain", which excludes IE < 9). + // Set to "cover", to make images cover all available space (requires + // support for background-size="cover", which excludes IE < 9): + stretchImages: false, + // Toggle the controls on pressing the Return key: + toggleControlsOnReturn: true, + // Toggle the controls on slide click: + toggleControlsOnSlideClick: true, + // Toggle the automatic slideshow interval on pressing the Space key: + toggleSlideshowOnSpace: true, + // Navigate the gallery by pressing left and right on the keyboard: + enableKeyboardNavigation: true, + // Close the gallery on pressing the Esc key: + closeOnEscape: true, + // Close the gallery when clicking on an empty slide area: + closeOnSlideClick: true, + // Close the gallery by swiping up or down: + closeOnSwipeUpOrDown: true, + // Emulate touch events on mouse-pointer devices such as desktop browsers: + emulateTouchEvents: true, + // Stop touch events from bubbling up to ancestor elements of the Gallery: + stopTouchEventsPropagation: false, + // Hide the page scrollbars: + hidePageScrollbars: true, + // Stops any touches on the container from scrolling the page: + disableScroll: true, + // Carousel mode (shortcut for carousel specific options): + carousel: false, + // Allow continuous navigation, moving from last to first + // and from first to last slide: + continuous: true, + // Remove elements outside of the preload range from the DOM: + unloadElements: true, + // Start with the automatic slideshow: + startSlideshow: false, + // Delay in milliseconds between slides for the automatic slideshow: + slideshowInterval: 5000, + // The starting index as integer. + // Can also be an object of the given list, + // or an equal object with the same url property: + index: 0, + // The number of elements to load around the current index: + preloadRange: 2, + // The transition speed between slide changes in milliseconds: + transitionSpeed: 400, + // The transition speed for automatic slide changes, set to an integer + // greater 0 to override the default transition speed: + slideshowTransitionSpeed: undefined, + // The event object for which the default action will be canceled + // on Gallery initialization (e.g. the click event to open the Gallery): + event: undefined, + // Callback function executed when the Gallery is initialized. + // Is called with the gallery instance as "this" object: + onopen: undefined, + // Callback function executed when the Gallery has been initialized + // and the initialization transition has been completed. + // Is called with the gallery instance as "this" object: + onopened: undefined, + // Callback function executed on slide change. + // Is called with the gallery instance as "this" object and the + // current index and slide as arguments: + onslide: undefined, + // Callback function executed after the slide change transition. + // Is called with the gallery instance as "this" object and the + // current index and slide as arguments: + onslideend: undefined, + // Callback function executed on slide content load. + // Is called with the gallery instance as "this" object and the + // slide index and slide element as arguments: + onslidecomplete: undefined, + // Callback function executed when the Gallery is about to be closed. + // Is called with the gallery instance as "this" object: + onclose: undefined, + // Callback function executed when the Gallery has been closed + // and the closing transition has been completed. + // Is called with the gallery instance as "this" object: + onclosed: undefined + }, + + carouselOptions: { + hidePageScrollbars: false, + toggleControlsOnReturn: false, + toggleSlideshowOnSpace: false, + enableKeyboardNavigation: false, + closeOnEscape: false, + closeOnSlideClick: false, + closeOnSwipeUpOrDown: false, + disableScroll: false, + startSlideshow: true + }, + + console: window.console && typeof window.console.log === 'function' + ? window.console + : {log: function () {}}, + + // Detect touch, transition, transform and background-size support: + support: (function (element) { + var support = { + touch: window.ontouchstart !== undefined || + (window.DocumentTouch && document instanceof DocumentTouch) + } + var transitions = { + webkitTransition: { + end: 'webkitTransitionEnd', + prefix: '-webkit-' + }, + MozTransition: { + end: 'transitionend', + prefix: '-moz-' + }, + OTransition: { + end: 'otransitionend', + prefix: '-o-' + }, + transition: { + end: 'transitionend', + prefix: '' + } + } + var prop + for (prop in transitions) { + if (transitions.hasOwnProperty(prop) && + element.style[prop] !== undefined) { + support.transition = transitions[prop] + support.transition.name = prop + break + } + } + function elementTests () { + var transition = support.transition + var prop + var translateZ + document.body.appendChild(element) + if (transition) { + prop = transition.name.slice(0, -9) + 'ransform' + if (element.style[prop] !== undefined) { + element.style[prop] = 'translateZ(0)' + translateZ = window.getComputedStyle(element) + .getPropertyValue(transition.prefix + 'transform') + support.transform = { + prefix: transition.prefix, + name: prop, + translate: true, + translateZ: !!translateZ && translateZ !== 'none' + } + } + } + if (element.style.backgroundSize !== undefined) { + support.backgroundSize = {} + element.style.backgroundSize = 'contain' + support.backgroundSize.contain = window + .getComputedStyle(element) + .getPropertyValue('background-size') === 'contain' + element.style.backgroundSize = 'cover' + support.backgroundSize.cover = window + .getComputedStyle(element) + .getPropertyValue('background-size') === 'cover' + } + document.body.removeChild(element) + } + if (document.body) { + elementTests() + } else { + $(document).on('DOMContentLoaded', elementTests) + } + return support + // Test element, has to be standard HTML and must not be hidden + // for the CSS3 tests using window.getComputedStyle to be applicable: + }(document.createElement('div'))), + + requestAnimationFrame: window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame, + + initialize: function () { + this.initStartIndex() + if (this.initWidget() === false) { + return false + } + this.initEventListeners() + // Load the slide at the given index: + this.onslide(this.index) + // Manually trigger the slideend event for the initial slide: + this.ontransitionend() + // Start the automatic slideshow if applicable: + if (this.options.startSlideshow) { + this.play() + } + }, + + slide: function (to, speed) { + window.clearTimeout(this.timeout) + var index = this.index + var direction + var naturalDirection + var diff + if (index === to || this.num === 1) { + return + } + if (!speed) { + speed = this.options.transitionSpeed + } + if (this.support.transform) { + if (!this.options.continuous) { + to = this.circle(to) + } + // 1: backward, -1: forward: + direction = Math.abs(index - to) / (index - to) + // Get the actual position of the slide: + if (this.options.continuous) { + naturalDirection = direction + direction = -this.positions[this.circle(to)] / this.slideWidth + // If going forward but to < index, use to = slides.length + to + // If going backward but to > index, use to = -slides.length + to + if (direction !== naturalDirection) { + to = -direction * this.num + to + } + } + diff = Math.abs(index - to) - 1 + // Move all the slides between index and to in the right direction: + while (diff) { + diff -= 1 + this.move( + this.circle((to > index ? to : index) - diff - 1), + this.slideWidth * direction, + 0 + ) + } + to = this.circle(to) + this.move(index, this.slideWidth * direction, speed) + this.move(to, 0, speed) + if (this.options.continuous) { + this.move( + this.circle(to - direction), + -(this.slideWidth * direction), + 0 + ) + } + } else { + to = this.circle(to) + this.animate(index * -this.slideWidth, to * -this.slideWidth, speed) + } + this.onslide(to) + }, + + getIndex: function () { + return this.index + }, + + getNumber: function () { + return this.num + }, + + prev: function () { + if (this.options.continuous || this.index) { + this.slide(this.index - 1) + } + }, + + next: function () { + if (this.options.continuous || this.index < this.num - 1) { + this.slide(this.index + 1) + } + }, + + play: function (time) { + var that = this + window.clearTimeout(this.timeout) + this.interval = time || this.options.slideshowInterval + if (this.elements[this.index] > 1) { + this.timeout = this.setTimeout( + (!this.requestAnimationFrame && this.slide) || function (to, speed) { + that.animationFrameId = that.requestAnimationFrame.call( + window, + function () { + that.slide(to, speed) + } + ) + }, + [this.index + 1, this.options.slideshowTransitionSpeed], + this.interval + ) + } + this.container.addClass(this.options.playingClass) + }, + + pause: function () { + window.clearTimeout(this.timeout) + this.interval = null + this.container.removeClass(this.options.playingClass) + }, + + add: function (list) { + var i + if (!list.concat) { + // Make a real array out of the list to add: + list = Array.prototype.slice.call(list) + } + if (!this.list.concat) { + // Make a real array out of the Gallery list: + this.list = Array.prototype.slice.call(this.list) + } + this.list = this.list.concat(list) + this.num = this.list.length + if (this.num > 2 && this.options.continuous === null) { + this.options.continuous = true + this.container.removeClass(this.options.leftEdgeClass) + } + this.container + .removeClass(this.options.rightEdgeClass) + .removeClass(this.options.singleClass) + for (i = this.num - list.length; i < this.num; i += 1) { + this.addSlide(i) + this.positionSlide(i) + } + this.positions.length = this.num + this.initSlides(true) + }, + + resetSlides: function () { + this.slidesContainer.empty() + this.unloadAllSlides() + this.slides = [] + }, + + handleClose: function () { + var options = this.options + this.destroyEventListeners() + // Cancel the slideshow: + this.pause() + this.container[0].style.display = 'none' + this.container + .removeClass(options.displayClass) + .removeClass(options.singleClass) + .removeClass(options.leftEdgeClass) + .removeClass(options.rightEdgeClass) + if (options.hidePageScrollbars) { + document.body.style.overflow = this.bodyOverflowStyle + } + if (this.options.clearSlides) { + this.resetSlides() + } + if (this.options.onclosed) { + this.options.onclosed.call(this) + } + }, + + close: function () { + var that = this + function closeHandler (event) { + if (event.target === that.container[0]) { + that.container.off( + that.support.transition.end, + closeHandler + ) + that.handleClose() + } + } + if (this.options.onclose) { + this.options.onclose.call(this) + } + if (this.support.transition && this.options.displayTransition) { + this.container.on( + this.support.transition.end, + closeHandler + ) + this.container.removeClass(this.options.displayClass) + } else { + this.handleClose() + } + }, + + circle: function (index) { + // Always return a number inside of the slides index range: + return (this.num + (index % this.num)) % this.num + }, + + move: function (index, dist, speed) { + this.translateX(index, dist, speed) + this.positions[index] = dist + }, + + translate: function (index, x, y, speed) { + var style = this.slides[index].style + var transition = this.support.transition + var transform = this.support.transform + style[transition.name + 'Duration'] = speed + 'ms' + style[transform.name] = 'translate(' + x + 'px, ' + y + 'px)' + + (transform.translateZ ? ' translateZ(0)' : '') + }, + + translateX: function (index, x, speed) { + this.translate(index, x, 0, speed) + }, + + translateY: function (index, y, speed) { + this.translate(index, 0, y, speed) + }, + + animate: function (from, to, speed) { + if (!speed) { + this.slidesContainer[0].style.left = to + 'px' + return + } + var that = this + var start = new Date().getTime() + var timer = window.setInterval(function () { + var timeElap = new Date().getTime() - start + if (timeElap > speed) { + that.slidesContainer[0].style.left = to + 'px' + that.ontransitionend() + window.clearInterval(timer) + return + } + that.slidesContainer[0].style.left = (((to - from) * + (Math.floor((timeElap / speed) * 100) / 100)) + + from) + 'px' + }, 4) + }, + + preventDefault: function (event) { + if (event.preventDefault) { + event.preventDefault() + } else { + event.returnValue = false + } + }, + + stopPropagation: function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } else { + event.cancelBubble = true + } + }, + + onresize: function () { + this.initSlides(true) + }, + + onmousedown: function (event) { + // Trigger on clicks of the left mouse button only + // and exclude video elements: + if (event.which && event.which === 1 && + event.target.nodeName !== 'VIDEO') { + // Preventing the default mousedown action is required + // to make touch emulation work with Firefox: + event.preventDefault() + ;(event.originalEvent || event).touches = [{ + pageX: event.pageX, + pageY: event.pageY + }] + this.ontouchstart(event) + } + }, + + onmousemove: function (event) { + if (this.touchStart) { + (event.originalEvent || event).touches = [{ + pageX: event.pageX, + pageY: event.pageY + }] + this.ontouchmove(event) + } + }, + + onmouseup: function (event) { + if (this.touchStart) { + this.ontouchend(event) + delete this.touchStart + } + }, + + onmouseout: function (event) { + if (this.touchStart) { + var target = event.target + var related = event.relatedTarget + if (!related || (related !== target && + !$.contains(target, related))) { + this.onmouseup(event) + } + } + }, + + ontouchstart: function (event) { + if (this.options.stopTouchEventsPropagation) { + this.stopPropagation(event) + } + // jQuery doesn't copy touch event properties by default, + // so we have to access the originalEvent object: + var touches = (event.originalEvent || event).touches[0] + this.touchStart = { + // Remember the initial touch coordinates: + x: touches.pageX, + y: touches.pageY, + // Store the time to determine touch duration: + time: Date.now() + } + // Helper variable to detect scroll movement: + this.isScrolling = undefined + // Reset delta values: + this.touchDelta = {} + }, + + ontouchmove: function (event) { + if (this.options.stopTouchEventsPropagation) { + this.stopPropagation(event) + } + // jQuery doesn't copy touch event properties by default, + // so we have to access the originalEvent object: + var touches = (event.originalEvent || event).touches[0] + var scale = (event.originalEvent || event).scale + var index = this.index + var touchDeltaX + var indices + // Ensure this is a one touch swipe and not, e.g. a pinch: + if (touches.length > 1 || (scale && scale !== 1)) { + return + } + if (this.options.disableScroll) { + event.preventDefault() + } + // Measure change in x and y coordinates: + this.touchDelta = { + x: touches.pageX - this.touchStart.x, + y: touches.pageY - this.touchStart.y + } + touchDeltaX = this.touchDelta.x + // Detect if this is a vertical scroll movement (run only once per touch): + if (this.isScrolling === undefined) { + this.isScrolling = this.isScrolling || + Math.abs(touchDeltaX) < Math.abs(this.touchDelta.y) + } + if (!this.isScrolling) { + // Always prevent horizontal scroll: + event.preventDefault() + // Stop the slideshow: + window.clearTimeout(this.timeout) + if (this.options.continuous) { + indices = [ + this.circle(index + 1), + index, + this.circle(index - 1) + ] + } else { + // Increase resistance if first slide and sliding left + // or last slide and sliding right: + this.touchDelta.x = touchDeltaX = + touchDeltaX / + ( + ((!index && touchDeltaX > 0) || + (index === this.num - 1 && touchDeltaX < 0)) + ? (Math.abs(touchDeltaX) / this.slideWidth + 1) + : 1 + ) + indices = [index] + if (index) { + indices.push(index - 1) + } + if (index < this.num - 1) { + indices.unshift(index + 1) + } + } + while (indices.length) { + index = indices.pop() + this.translateX(index, touchDeltaX + this.positions[index], 0) + } + } else if (this.options.closeOnSwipeUpOrDown) { + this.translateY(index, this.touchDelta.y + this.positions[index], 0) + } + }, + + ontouchend: function (event) { + if (this.options.stopTouchEventsPropagation) { + this.stopPropagation(event) + } + var index = this.index + var speed = this.options.transitionSpeed + var slideWidth = this.slideWidth + var isShortDuration = Number(Date.now() - this.touchStart.time) < 250 + // Determine if slide attempt triggers next/prev slide: + var isValidSlide = + (isShortDuration && Math.abs(this.touchDelta.x) > 20) || + Math.abs(this.touchDelta.x) > slideWidth / 2 + // Determine if slide attempt is past start or end: + var isPastBounds = (!index && this.touchDelta.x > 0) || + (index === this.num - 1 && this.touchDelta.x < 0) + var isValidClose = !isValidSlide && this.options.closeOnSwipeUpOrDown && + ((isShortDuration && Math.abs(this.touchDelta.y) > 20) || + Math.abs(this.touchDelta.y) > this.slideHeight / 2) + var direction + var indexForward + var indexBackward + var distanceForward + var distanceBackward + if (this.options.continuous) { + isPastBounds = false + } + // Determine direction of swipe (true: right, false: left): + direction = this.touchDelta.x < 0 ? -1 : 1 + if (!this.isScrolling) { + if (isValidSlide && !isPastBounds) { + indexForward = index + direction + indexBackward = index - direction + distanceForward = slideWidth * direction + distanceBackward = -slideWidth * direction + if (this.options.continuous) { + this.move(this.circle(indexForward), distanceForward, 0) + this.move(this.circle(index - 2 * direction), distanceBackward, 0) + } else if (indexForward >= 0 && + indexForward < this.num) { + this.move(indexForward, distanceForward, 0) + } + this.move(index, this.positions[index] + distanceForward, speed) + this.move( + this.circle(indexBackward), + this.positions[this.circle(indexBackward)] + distanceForward, + speed + ) + index = this.circle(indexBackward) + this.onslide(index) + } else { + // Move back into position + if (this.options.continuous) { + this.move(this.circle(index - 1), -slideWidth, speed) + this.move(index, 0, speed) + this.move(this.circle(index + 1), slideWidth, speed) + } else { + if (index) { + this.move(index - 1, -slideWidth, speed) + } + this.move(index, 0, speed) + if (index < this.num - 1) { + this.move(index + 1, slideWidth, speed) + } + } + } + } else { + if (isValidClose) { + this.close() + } else { + // Move back into position + this.translateY(index, 0, speed) + } + } + }, + + ontouchcancel: function (event) { + if (this.touchStart) { + this.ontouchend(event) + delete this.touchStart + } + }, + + ontransitionend: function (event) { + var slide = this.slides[this.index] + if (!event || slide === event.target) { + if (this.interval) { + this.play() + } + this.setTimeout( + this.options.onslideend, + [this.index, slide] + ) + } + }, + + oncomplete: function (event) { + var target = event.target || event.srcElement + var parent = target && target.parentNode + var index + if (!target || !parent) { + return + } + index = this.getNodeIndex(parent) + $(parent).removeClass(this.options.slideLoadingClass) + if (event.type === 'error') { + $(parent).addClass(this.options.slideErrorClass) + this.elements[index] = 3 // Fail + } else { + this.elements[index] = 2 // Done + } + // Fix for IE7's lack of support for percentage max-height: + if (target.clientHeight > this.container[0].clientHeight) { + target.style.maxHeight = this.container[0].clientHeight + } + if (this.interval && this.slides[this.index] === parent) { + this.play() + } + this.setTimeout( + this.options.onslidecomplete, + [index, parent] + ) + }, + + onload: function (event) { + this.oncomplete(event) + }, + + onerror: function (event) { + this.oncomplete(event) + }, + + onkeydown: function (event) { + switch (event.which || event.keyCode) { + case 13: // Return + if (this.options.toggleControlsOnReturn) { + this.preventDefault(event) + this.toggleControls() + } + break + case 27: // Esc + if (this.options.closeOnEscape) { + this.close() + // prevent Esc from closing other things + event.stopImmediatePropagation() + } + break + case 32: // Space + if (this.options.toggleSlideshowOnSpace) { + this.preventDefault(event) + this.toggleSlideshow() + } + break + case 37: // Left + if (this.options.enableKeyboardNavigation) { + this.preventDefault(event) + this.prev() + } + break + case 38: // Up + if (this.options.enableKeyboardNavigation) { + this.preventDefault(event) + console.log("UP Key") + } + break + case 39: // Right + if (this.options.enableKeyboardNavigation) { + this.preventDefault(event) + this.next() + } + break + case 40: // Down + if (this.options.enableKeyboardNavigation) { + this.preventDefault(event) + console.log("Down Key") + } + break + } + }, + + handleClick: function (event) { + var options = this.options + var target = event.target || event.srcElement + var parent = target.parentNode + function isTarget (className) { + return $(target).hasClass(className) || + $(parent).hasClass(className) + } + if (isTarget(options.toggleClass)) { + // Click on "toggle" control + this.preventDefault(event) + this.toggleControls() + } else if (isTarget(options.prevClass)) { + // Click on "prev" control + this.preventDefault(event) + this.prev() + } else if (isTarget(options.nextClass)) { + // Click on "next" control + this.preventDefault(event) + this.next() + } else if (isTarget(options.closeClass)) { + // Click on "close" control + this.preventDefault(event) + this.close() + } else if (isTarget(options.playPauseClass)) { + // Click on "play-pause" control + this.preventDefault(event) + this.toggleSlideshow() + } else if (parent === this.slidesContainer[0]) { + // Click on slide background + if (options.closeOnSlideClick) { + this.preventDefault(event) + this.close() + } else if (options.toggleControlsOnSlideClick) { + this.preventDefault(event) + this.toggleControls() + } + } else if (parent.parentNode && + parent.parentNode === this.slidesContainer[0]) { + // Click on displayed element + if (options.toggleControlsOnSlideClick) { + this.preventDefault(event) + this.toggleControls() + } + } + }, + + onclick: function (event) { + if (this.options.emulateTouchEvents && + this.touchDelta && (Math.abs(this.touchDelta.x) > 20 || + Math.abs(this.touchDelta.y) > 20)) { + delete this.touchDelta + return + } + return this.handleClick(event) + }, + + updateEdgeClasses: function (index) { + if (!index) { + this.container.addClass(this.options.leftEdgeClass) + } else { + this.container.removeClass(this.options.leftEdgeClass) + } + if (index === this.num - 1) { + this.container.addClass(this.options.rightEdgeClass) + } else { + this.container.removeClass(this.options.rightEdgeClass) + } + }, + + handleSlide: function (index) { + if (!this.options.continuous) { + this.updateEdgeClasses(index) + } + this.loadElements(index) + if (this.options.unloadElements) { + this.unloadElements(index) + } + this.setTitle(index) + }, + + onslide: function (index) { + this.index = index + this.handleSlide(index) + this.setTimeout(this.options.onslide, [index, this.slides[index]]) + }, + + setTitle: function (index) { + var text = this.slides[index].firstChild.title + var titleElement = this.titleElement + if (titleElement.length) { + this.titleElement.empty() + if (text) { + titleElement[0].appendChild(document.createTextNode(text)) + } + } + }, + + setTimeout: function (func, args, wait) { + var that = this + return func && window.setTimeout(function () { + func.apply(that, args || []) + }, wait || 0) + }, + + imageFactory: function (obj, callback) { + var that = this + var img = this.imagePrototype.cloneNode(false) + var url = obj + var backgroundSize = this.options.stretchImages + var called + var element + var title + function callbackWrapper (event) { + if (!called) { + event = { + type: event.type, + target: element + } + if (!element.parentNode) { + // Fix for IE7 firing the load event for + // cached images before the element could + // be added to the DOM: + return that.setTimeout(callbackWrapper, [event]) + } + called = true + $(img).off('load error', callbackWrapper) + if (backgroundSize) { + if (event.type === 'load') { + element.style.background = 'url("' + url + + '") center no-repeat' + element.style.backgroundSize = backgroundSize + } + } + callback(event) + } + } + if (typeof url !== 'string') { + url = this.getItemProperty(obj, this.options.urlProperty) + title = this.getItemProperty(obj, this.options.titleProperty) + } + if (backgroundSize === true) { + backgroundSize = 'contain' + } + backgroundSize = this.support.backgroundSize && + this.support.backgroundSize[backgroundSize] && backgroundSize + if (backgroundSize) { + element = this.elementPrototype.cloneNode(false) + } else { + element = img + img.draggable = false + } + if (title) { + element.title = title + } + $(img).on('load error', callbackWrapper) + img.src = url + return element + }, + + createElement: function (obj, callback) { + var type = obj && this.getItemProperty(obj, this.options.typeProperty) + var factory = (type && this[type.split('/')[0] + 'Factory']) || + this.imageFactory + var element = obj && factory.call(this, obj, callback) + var srcset = this.getItemProperty(obj, this.options.srcsetProperty) + if (!element) { + element = this.elementPrototype.cloneNode(false) + this.setTimeout(callback, [{ + type: 'error', + target: element + }]) + } + if (srcset) { + element.setAttribute('srcset', srcset) + } + $(element).addClass(this.options.slideContentClass) + return element + }, + + loadElement: function (index) { + if (!this.elements[index]) { + if (this.slides[index].firstChild) { + this.elements[index] = $(this.slides[index]) + .hasClass(this.options.slideErrorClass) ? 3 : 2 + } else { + this.elements[index] = 1 // Loading + $(this.slides[index]).addClass(this.options.slideLoadingClass) + this.slides[index].appendChild(this.createElement( + this.list[index], + this.proxyListener + )) + } + } + }, + + loadElements: function (index) { + var limit = Math.min(this.num, this.options.preloadRange * 2 + 1) + var j = index + var i + for (i = 0; i < limit; i += 1) { + // First load the current slide element (0), + // then the next one (+1), + // then the previous one (-2), + // then the next after next (+2), etc.: + j += i * (i % 2 === 0 ? -1 : 1) + // Connect the ends of the list to load slide elements for + // continuous navigation: + j = this.circle(j) + this.loadElement(j) + } + }, + + unloadElements: function (index) { + var i, + diff + for (i in this.elements) { + if (this.elements.hasOwnProperty(i)) { + diff = Math.abs(index - i) + if (diff > this.options.preloadRange && + diff + this.options.preloadRange < this.num) { + this.unloadSlide(i) + delete this.elements[i] + } + } + } + }, + + addSlide: function (index) { + var slide = this.slidePrototype.cloneNode(false) + slide.setAttribute('data-index', index) + this.slidesContainer[0].appendChild(slide) + this.slides.push(slide) + }, + + positionSlide: function (index) { + var slide = this.slides[index] + slide.style.width = this.slideWidth + 'px' + if (this.support.transform) { + slide.style.left = (index * -this.slideWidth) + 'px' + this.move( + index, this.index > index + ? -this.slideWidth + : (this.index < index ? this.slideWidth : 0), + 0 + ) + } + }, + + initSlides: function (reload) { + var clearSlides, + i + if (!reload) { + this.positions = [] + this.positions.length = this.num + this.elements = {} + this.imagePrototype = document.createElement('img') + this.elementPrototype = document.createElement('div') + this.slidePrototype = document.createElement('div') + $(this.slidePrototype).addClass(this.options.slideClass) + this.slides = this.slidesContainer[0].children + clearSlides = this.options.clearSlides || + this.slides.length !== this.num + } + this.slideWidth = this.container[0].offsetWidth + this.slideHeight = this.container[0].offsetHeight + this.slidesContainer[0].style.width = + (this.num * this.slideWidth) + 'px' + if (clearSlides) { + this.resetSlides() + } + for (i = 0; i < this.num; i += 1) { + if (clearSlides) { + this.addSlide(i) + } + this.positionSlide(i) + } + // Reposition the slides before and after the given index: + if (this.options.continuous && this.support.transform) { + this.move(this.circle(this.index - 1), -this.slideWidth, 0) + this.move(this.circle(this.index + 1), this.slideWidth, 0) + } + if (!this.support.transform) { + this.slidesContainer[0].style.left = + (this.index * -this.slideWidth) + 'px' + } + }, + + unloadSlide: function (index) { + var slide, + firstChild + slide = this.slides[index] + firstChild = slide.firstChild + if (firstChild !== null) { + slide.removeChild(firstChild) + } + }, + + unloadAllSlides: function () { + var i, + len + for (i = 0, len = this.slides.length; i < len; i++) { + this.unloadSlide(i) + } + }, + + toggleControls: function () { + var controlsClass = this.options.controlsClass + if (this.container.hasClass(controlsClass)) { + this.container.removeClass(controlsClass) + } else { + this.container.addClass(controlsClass) + } + }, + + toggleSlideshow: function () { + if (!this.interval) { + this.play() + } else { + this.pause() + } + }, + + getNodeIndex: function (element) { + return parseInt(element.getAttribute('data-index'), 10) + }, + + getNestedProperty: function (obj, property) { + property.replace( + // Matches native JavaScript notation in a String, + // e.g. '["doubleQuoteProp"].dotProp[2]' + /\[(?:'([^']+)'|"([^"]+)"|(\d+))\]|(?:(?:^|\.)([^\.\[]+))/g, + function (str, singleQuoteProp, doubleQuoteProp, arrayIndex, dotProp) { + var prop = dotProp || singleQuoteProp || doubleQuoteProp || + (arrayIndex && parseInt(arrayIndex, 10)) + if (str && obj) { + obj = obj[prop] + } + } + ) + return obj + }, + + getDataProperty: function (obj, property) { + if (obj.getAttribute) { + var prop = obj.getAttribute('data-' + + property.replace(/([A-Z])/g, '-$1').toLowerCase()) + if (typeof prop === 'string') { + if (/^(true|false|null|-?\d+(\.\d+)?|\{[\s\S]*\}|\[[\s\S]*\])$/ + .test(prop)) { + try { + return $.parseJSON(prop) + } catch (ignore) {} + } + return prop + } + } + }, + + getItemProperty: function (obj, property) { + var prop = obj[property] + if (prop === undefined) { + prop = this.getDataProperty(obj, property) + if (prop === undefined) { + prop = this.getNestedProperty(obj, property) + } + } + return prop + }, + + initStartIndex: function () { + var index = this.options.index + var urlProperty = this.options.urlProperty + var i + // Check if the index is given as a list object: + if (index && typeof index !== 'number') { + for (i = 0; i < this.num; i += 1) { + if (this.list[i] === index || + this.getItemProperty(this.list[i], urlProperty) === + this.getItemProperty(index, urlProperty)) { + index = i + break + } + } + } + // Make sure the index is in the list range: + this.index = this.circle(parseInt(index, 10) || 0) + }, + + initEventListeners: function () { + var that = this + var slidesContainer = this.slidesContainer + function proxyListener (event) { + var type = that.support.transition && + that.support.transition.end === event.type + ? 'transitionend' + : event.type + that['on' + type](event) + } + $(window).on('resize', proxyListener) + $(document.body).on('keydown', proxyListener) + this.container.on('click', proxyListener) + if (this.support.touch) { + slidesContainer + .on('touchstart touchmove touchend touchcancel', proxyListener) + } else if (this.options.emulateTouchEvents && + this.support.transition) { + slidesContainer + .on('mousedown mousemove mouseup mouseout', proxyListener) + } + if (this.support.transition) { + slidesContainer.on( + this.support.transition.end, + proxyListener + ) + } + this.proxyListener = proxyListener + }, + + destroyEventListeners: function () { + var slidesContainer = this.slidesContainer + var proxyListener = this.proxyListener + $(window).off('resize', proxyListener) + $(document.body).off('keydown', proxyListener) + this.container.off('click', proxyListener) + if (this.support.touch) { + slidesContainer + .off('touchstart touchmove touchend touchcancel', proxyListener) + } else if (this.options.emulateTouchEvents && + this.support.transition) { + slidesContainer + .off('mousedown mousemove mouseup mouseout', proxyListener) + } + if (this.support.transition) { + slidesContainer.off( + this.support.transition.end, + proxyListener + ) + } + }, + + handleOpen: function () { + if (this.options.onopened) { + this.options.onopened.call(this) + } + }, + + initWidget: function () { + var that = this + function openHandler (event) { + if (event.target === that.container[0]) { + that.container.off( + that.support.transition.end, + openHandler + ) + that.handleOpen() + } + } + this.container = $(this.options.container) + if (!this.container.length) { + this.console.log( + 'blueimp Gallery: Widget container not found.', + this.options.container + ) + return false + } + this.slidesContainer = this.container.find( + this.options.slidesContainer + ).first() + if (!this.slidesContainer.length) { + this.console.log( + 'blueimp Gallery: Slides container not found.', + this.options.slidesContainer + ) + return false + } + this.titleElement = this.container.find( + this.options.titleElement + ).first() + if (this.num === 1) { + this.container.addClass(this.options.singleClass) + } + if (this.options.onopen) { + this.options.onopen.call(this) + } + if (this.support.transition && this.options.displayTransition) { + this.container.on( + this.support.transition.end, + openHandler + ) + } else { + this.handleOpen() + } + if (this.options.hidePageScrollbars) { + // Hide the page scrollbars: + this.bodyOverflowStyle = document.body.style.overflow + document.body.style.overflow = 'hidden' + } + this.container[0].style.display = 'block' + this.initSlides() + this.container.addClass(this.options.displayClass) + }, + + initOptions: function (options) { + // Create a copy of the prototype options: + this.options = $.extend({}, this.options) + // Check if carousel mode is enabled: + if ((options && options.carousel) || + (this.options.carousel && (!options || options.carousel !== false))) { + $.extend(this.options, this.carouselOptions) + } + // Override any given options: + $.extend(this.options, options) + if (this.num < 3) { + // 1 or 2 slides cannot be displayed continuous, + // remember the original option by setting to null instead of false: + this.options.continuous = this.options.continuous ? null : false + } + if (!this.support.transition) { + this.options.emulateTouchEvents = false + } + if (this.options.event) { + this.preventDefault(this.options.event) + } + } + + }) + + return Gallery +}))