From 1a76d647812802d2ac1973b06ba6f8a5e334d6f4 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 6 Mar 2017 11:32:27 +0530 Subject: [PATCH] ListView refactor (#2715) * [list_settings] save last_view, kanban_board, gantt_mode * listview.js cleanup * image_view refactor * image_view done * wip * [photoswipe] wip * show view for calendar, kanban even if no results * [gantt_view] refactor into separate file * [imageview] 3 column border fix * [imageview] gallery working * delete old libs * indentation to tabs * [gantt] update lib, custom popup html * custom fontawesome checkbox * reset gantt state when not permitted * checkbox styling fix * working commit * image, calendar, gantt view working * more refactoring, kanban view * minor * removed old files * user settings improved * filters and sort selector de-coupling * wip * [imageview] white pswp background * kanban filters saving fixed * fixed reportview * minor * removed listing.js * minor fixes and cleanup * patch for UserSettings table * patch fix --- .../communication/communication_list.js | 1 - frappe/core/doctype/file/file_list.js | 4 +- frappe/desk/form/load.py | 4 +- frappe/desk/form/meta.py | 2 +- frappe/desk/reportview.py | 6 +- frappe/email/page/email_inbox/email_inbox.js | 16 +- frappe/email/page/email_inbox/inbox_list.html | 2 +- frappe/installer.py | 2 +- frappe/model/db_query.py | 34 +- frappe/model/utils/list_settings.py | 39 - frappe/model/utils/user_settings.py | 48 + frappe/patches.txt | 2 +- frappe/patches/v8_0/__init__.py | 0 .../rename_listsettings_to_usersettings.py | 26 + frappe/public/build.json | 26 +- frappe/public/css/desk.css | 26 + frappe/public/css/form_grid.css | 1 + frappe/public/css/list.css | 169 +- frappe/public/css/sidebar.css | 3 + frappe/public/images/default-skin.svg | 1 + frappe/public/js/frappe/form/sidebar.js | 2 +- .../frappe/form/templates/form_sidebar.html | 4 +- .../js/frappe/list/blueimp-gallery.html | 9 - frappe/public/js/frappe/list/doclistview.js | 1007 ----- .../list/header_select_all_like_filter.html | 9 +- .../js/frappe/list/image_view_item_row.html | 44 - frappe/public/js/frappe/list/imageview.js | 146 - .../public/js/frappe/list/list_item_main.html | 7 +- .../js/frappe/list/list_item_main_head.html | 5 +- .../public/js/frappe/list/list_item_row.html | 8 +- .../js/frappe/list/list_item_row_head.html | 2 +- .../js/frappe/list/list_item_subject.html | 4 +- frappe/public/js/frappe/list/list_renderer.js | 528 +++ frappe/public/js/frappe/list/list_sidebar.js | 22 +- frappe/public/js/frappe/list/list_view.js | 853 ++++ frappe/public/js/frappe/list/listview.js | 403 -- frappe/public/js/frappe/misc/utils.js | 70 +- frappe/public/js/frappe/model/model.js | 9 +- .../public/js/frappe/model/user_settings.js | 41 + frappe/public/js/frappe/router.js | 3 +- frappe/public/js/frappe/ui/base_list.js | 434 ++ frappe/public/js/frappe/ui/filters/filters.js | 46 +- frappe/public/js/frappe/ui/listing.html | 13 - frappe/public/js/frappe/ui/listing.js | 58 +- frappe/public/js/frappe/ui/page.js | 1 - frappe/public/js/frappe/ui/sort_selector.js | 30 +- .../js/frappe/ui/toolbar/awesome_bar.js | 2 +- .../js/frappe/ui/toolbar/notifications.js | 2 +- .../frappe/views/{ => calendar}/calendar.js | 47 +- frappe/public/js/frappe/views/container.js | 8 +- frappe/public/js/frappe/views/factory.js | 2 +- .../js/frappe/views/gantt/gantt_view.js | 201 + .../js/frappe/views/image/image_view.js | 180 + .../image}/image_view_item_main_head.html | 2 + .../views/image/image_view_item_row.html | 44 + .../js/frappe/views/image/photoswipe_dom.html | 69 + .../js/frappe/views/kanban/kanban_board.js | 1048 +++++ .../js/frappe/views/kanban/kanban_view.js | 1100 +---- .../js/frappe/views/reports/reportview.js | 28 +- frappe/public/js/legacy/form.js | 2 +- .../js/lib/frappe-gantt/frappe-gantt.js | 80 +- .../gallery/css/blueimp-gallery-indicator.css | 72 - .../js/lib/gallery/css/blueimp-gallery.css | 226 - .../gallery/js/blueimp-gallery-indicator.js | 155 - .../js/lib/gallery/js/blueimp-gallery.js | 1399 ------- .../public/js/lib/photoswipe/default-skin.css | 483 +++ .../lib/photoswipe/photoswipe-ui-default.js | 861 ++++ .../public/js/lib/photoswipe/photoswipe.css | 178 + frappe/public/js/lib/photoswipe/photoswipe.js | 3718 +++++++++++++++++ frappe/public/less/desk.less | 28 + frappe/public/less/form_grid.less | 1 + frappe/public/less/list.less | 220 +- frappe/public/less/sidebar.less | 4 + 73 files changed, 9453 insertions(+), 4877 deletions(-) delete mode 100644 frappe/model/utils/list_settings.py create mode 100644 frappe/model/utils/user_settings.py create mode 100644 frappe/patches/v8_0/__init__.py create mode 100644 frappe/patches/v8_0/rename_listsettings_to_usersettings.py create mode 100755 frappe/public/images/default-skin.svg delete mode 100644 frappe/public/js/frappe/list/blueimp-gallery.html delete mode 100644 frappe/public/js/frappe/list/doclistview.js delete mode 100644 frappe/public/js/frappe/list/image_view_item_row.html delete mode 100644 frappe/public/js/frappe/list/imageview.js create mode 100644 frappe/public/js/frappe/list/list_renderer.js create mode 100644 frappe/public/js/frappe/list/list_view.js delete mode 100644 frappe/public/js/frappe/list/listview.js create mode 100644 frappe/public/js/frappe/model/user_settings.js create mode 100644 frappe/public/js/frappe/ui/base_list.js rename frappe/public/js/frappe/views/{ => calendar}/calendar.js (89%) create mode 100644 frappe/public/js/frappe/views/gantt/gantt_view.js create mode 100644 frappe/public/js/frappe/views/image/image_view.js rename frappe/public/js/frappe/{list => views/image}/image_view_item_main_head.html (77%) create mode 100644 frappe/public/js/frappe/views/image/image_view_item_row.html create mode 100644 frappe/public/js/frappe/views/image/photoswipe_dom.html create mode 100644 frappe/public/js/frappe/views/kanban/kanban_board.js delete mode 100644 frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css delete mode 100644 frappe/public/js/lib/gallery/css/blueimp-gallery.css delete mode 100644 frappe/public/js/lib/gallery/js/blueimp-gallery-indicator.js delete mode 100644 frappe/public/js/lib/gallery/js/blueimp-gallery.js create mode 100755 frappe/public/js/lib/photoswipe/default-skin.css create mode 100755 frappe/public/js/lib/photoswipe/photoswipe-ui-default.js create mode 100755 frappe/public/js/lib/photoswipe/photoswipe.css create mode 100755 frappe/public/js/lib/photoswipe/photoswipe.js diff --git a/frappe/core/doctype/communication/communication_list.js b/frappe/core/doctype/communication/communication_list.js index b352317e2d..458991f52f 100644 --- a/frappe/core/doctype/communication/communication_list.js +++ b/frappe/core/doctype/communication/communication_list.js @@ -1,5 +1,4 @@ frappe.listview_settings['Communication'] = { add_fields: ["sent_or_received", "recipients", "subject", "communication_medium", "communication_type"], - //default_filters: [["Communication", "communication_type", "=", "Communication"]], filters: [["status", "=", "Open"]] }; diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/file/file_list.js index 135ad1e68d..3f94592e91 100644 --- a/frappe/core/doctype/file/file_list.js +++ b/frappe/core/doctype/file/file_list.js @@ -42,7 +42,7 @@ frappe.listview_settings['File'] = { doclist.listview.settings.setup_menu(doclist); doclist.listview.settings.setup_dragdrop(doclist); - doclist.$page.on("click", ".list-delete", function(event) { + doclist.$page.on("click", ".list-row-checkbox", function(event) { doclist.listview.settings.add_menu_item_copy(doclist); }) }, @@ -127,7 +127,7 @@ frappe.listview_settings['File'] = { add_menu_item_copy: function(doclist){ if (!doclist.copy) { var copy_menu = doclist.page.add_menu_item(__("Copy"), function() { - if(doclist.$page.find(".list-delete:checked").length){ + if(doclist.$page.find(".list-row-checkbox:checked").length){ doclist.selected_files = doclist.get_checked_items(); doclist.old_parent = doclist.current_folder; doclist.listview.settings.add_menu_item_paste(doclist); diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index e39ea49d78..0211244eae 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -7,7 +7,7 @@ import frappe.utils import frappe.share import frappe.defaults import frappe.desk.form.meta -from frappe.model.utils.list_settings import get_list_settings +from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe import _ @@ -70,7 +70,7 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): docs = get_meta_bundle(doctype) frappe.response['user_permissions'] = get_user_permissions(docs) - frappe.response['list_settings'] = get_list_settings(parent_dt or doctype) + frappe.response['user_settings'] = get_user_settings(parent_dt or doctype) if cached_timestamp and docs[0].modified==cached_timestamp: return "use_cache" diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 703f2ff56c..90b977d82d 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -176,7 +176,7 @@ class FormMeta(Meta): def load_kanban_boards(self): kanban_boards = frappe.get_all( - 'Kanban Board', filters={'reference_doctype': self.name}) + 'Kanban Board', fields=['name', 'filters', 'reference_doctype'], filters={'reference_doctype': self.name}) self.set("__kanban_boards", kanban_boards, as_value=True) def load_kanban_column_fields(self): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 9225b8ed92..8f9fead731 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -32,10 +32,10 @@ def get_form_params(): data["fields"] = json.loads(data["fields"]) if isinstance(data.get("docstatus"), basestring): data["docstatus"] = json.loads(data["docstatus"]) - if isinstance(data.get("save_list_settings"), basestring): - data["save_list_settings"] = json.loads(data["save_list_settings"]) + if isinstance(data.get("save_user_settings"), basestring): + data["save_user_settings"] = json.loads(data["save_user_settings"]) else: - data["save_list_settings"] = True + data["save_user_settings"] = True # queries must always be server side diff --git a/frappe/email/page/email_inbox/email_inbox.js b/frappe/email/page/email_inbox/email_inbox.js index e794951b67..a692687d63 100644 --- a/frappe/email/page/email_inbox/email_inbox.js +++ b/frappe/email/page/email_inbox/email_inbox.js @@ -126,7 +126,7 @@ frappe.Inbox = frappe.ui.Listing.extend({ $(me.page.sidebar).find(".list-row").removeClass("list-row-head").css("font-weight","normal"); $(btn.currentTarget).closest(".list-row").addClass("list-row-head").css("font-weight","bold"); me.cur_page = 1; - $(me.page.main).find(".list-select-all,.list-delete").prop("checked",false); + $(me.page.main).find(".list-select-all,.list-row-checkbox").prop("checked",false); me.toggle_actions(); if(me.account=="Sent"){ @@ -518,11 +518,11 @@ frappe.Inbox = frappe.ui.Listing.extend({ var me = this; $(".list-select-all").on("click", function () { - $(me.wrapper).find('.list-delete').prop("checked", $(this).prop("checked")); + $(me.wrapper).find('.list-row-checkbox').prop("checked", $(this).prop("checked")); me.toggle_actions(); }); - $(me.wrapper).on("click", ".list-delete", function (event) { + $(me.wrapper).on("click", ".list-row-checkbox", function (event) { me.toggle_actions(); // multi-select using shift key @@ -530,9 +530,9 @@ frappe.Inbox = frappe.ui.Listing.extend({ if (event.shiftKey && $this.prop("checked")) { var $end_row = $this.parents(".list-row"); var $start_row = $end_row.prevAll(".list-row") - .find(".list-delete:checked").last().parents(".list-row"); + .find(".list-row-checkbox:checked").last().parents(".list-row"); if ($start_row) { - $start_row.nextUntil($end_row).find(".list-delete").prop("checked", true); + $start_row.nextUntil($end_row).find(".list-row-checkbox").prop("checked", true); } } }); @@ -563,7 +563,7 @@ frappe.Inbox = frappe.ui.Listing.extend({ }, toggle_actions: function () { var me = this; - if (me.page.main.find(".list-delete:checked").length) { + if (me.page.main.find(".list-row-checkbox:checked").length) { //show buttons $(me.page.actions_btn_group).show(); $(me.page.btn_primary).hide() @@ -625,10 +625,10 @@ frappe.Inbox = frappe.ui.Listing.extend({ val:val } }) - $('.list-delete:checked').prop( "checked", false ); + $('.list-row-checkbox:checked').prop( "checked", false ); }, action_checked_items: function(action) { - return $.map(this.page.main.find('.list-delete:checked'), function(e) { + return $.map(this.page.main.find('.list-row-checkbox:checked'), function(e) { return eval('$(e).closest(".row-named")'+action); }); }, diff --git a/frappe/email/page/email_inbox/inbox_list.html b/frappe/email/page/email_inbox/inbox_list.html index b438f7dbfb..8e94f85ae5 100644 --- a/frappe/email/page/email_inbox/inbox_list.html +++ b/frappe/email/page/email_inbox/inbox_list.html @@ -4,7 +4,7 @@
- + {% if (data.sender_full_name){var sender = data.sender_full_name} else {var sender = data.sender} %} {%= sender %} diff --git a/frappe/installer.py b/frappe/installer.py index 873e570119..19af33ae1a 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -71,7 +71,7 @@ def create_database_and_user(force, verbose): frappe.db.close() def create_list_settings_table(): - frappe.db.sql_ddl("""create table if not exists __ListSettings ( + frappe.db.sql_ddl("""create table if not exists __UserSettings ( `user` VARCHAR(180) NOT NULL, `doctype` VARCHAR(180) NOT NULL, `data` TEXT, diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index d5be376913..5193e8faa7 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -11,7 +11,7 @@ import frappe.permissions from frappe.utils import flt, cint, getdate, get_datetime, get_time, make_filter_tuple, get_filter, add_to_date from frappe import _ from frappe.model import optional_fields -from frappe.model.utils.list_settings import get_list_settings, update_list_settings +from frappe.model.utils.user_settings import get_user_settings, update_user_settings from datetime import datetime class DatabaseQuery(object): @@ -30,8 +30,8 @@ class DatabaseQuery(object): limit_page_length=None, as_list=False, with_childnames=False, debug=False, ignore_permissions=False, user=None, with_comment_count=False, join='left join', distinct=False, start=None, page_length=None, limit=None, - ignore_ifnull=False, save_list_settings=False, save_list_settings_fields=False, - update=None, add_total_row=None): + ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, + update=None, add_total_row=None, user_settings=None): if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): raise frappe.PermissionError, self.doctype @@ -72,8 +72,10 @@ class DatabaseQuery(object): self.flags.ignore_permissions = ignore_permissions self.user = user or frappe.session.user self.update = update - self.list_settings_fields = copy.deepcopy(self.fields) + self.user_settings_fields = copy.deepcopy(self.fields) #self.debug = True + if user_settings: + self.user_settings = json.loads(user_settings) if query: result = self.run_custom_query(query) @@ -83,9 +85,9 @@ class DatabaseQuery(object): if with_comment_count and not as_list and self.doctype: self.add_comment_count(result) - if save_list_settings: - self.save_list_settings_fields = save_list_settings_fields - self.update_list_settings() + if save_user_settings: + self.save_user_settings_fields = save_user_settings_fields + self.update_user_settings() return result @@ -513,17 +515,17 @@ class DatabaseQuery(object): if "_comments" in r: r._comment_count = len(json.loads(r._comments or "[]")) - def update_list_settings(self): - # update list settings if new search - list_settings = json.loads(get_list_settings(self.doctype) or '{}') - list_settings['filters'] = self.filters - list_settings['limit'] = self.limit_page_length - list_settings['order_by'] = self.order_by + def update_user_settings(self): + # update user settings if new search + user_settings = json.loads(get_user_settings(self.doctype)) - if self.save_list_settings_fields: - list_settings['fields'] = self.list_settings_fields + if hasattr(self, 'user_settings'): + user_settings.update(self.user_settings) - update_list_settings(self.doctype, list_settings) + if self.save_user_settings_fields: + user_settings['fields'] = self.user_settings_fields + + update_user_settings(self.doctype, user_settings) def get_order_by(doctype, meta): order_by = "" diff --git a/frappe/model/utils/list_settings.py b/frappe/model/utils/list_settings.py deleted file mode 100644 index 0f6b91359e..0000000000 --- a/frappe/model/utils/list_settings.py +++ /dev/null @@ -1,39 +0,0 @@ -import frappe, json - -def get_list_settings(doctype, for_update=False): - list_settings = frappe.cache().hget('_list_settings', - '{0}::{1}'.format(doctype, frappe.session.user)) - - if list_settings is None: - list_settings = frappe.db.sql('''select data from __ListSettings - where user=%s and doctype=%s''', (frappe.session.user, doctype)) - list_settings = list_settings and list_settings[0][0] or '{}' - - if not for_update: - update_list_settings(doctype, list_settings, True) - - return list_settings - -def update_list_settings(doctype, list_settings, for_update=False): - '''update list settings in cache''' - - if for_update: - current = json.loads(list_settings) - else: - current = json.loads(get_list_settings(doctype, for_update = True)) - - if isinstance(current, basestring): - # corrupt due to old code, remove this in a future release - current = {} - - current.update(list_settings) - - frappe.cache().hset('_list_settings', '{0}::{1}'.format(doctype, frappe.session.user), - json.dumps(current)) - -def sync_list_settings(): - '''Sync from cache to database (called asynchronously via the browser)''' - for key, data in frappe.cache().hgetall('_list_settings').iteritems(): - doctype, user = key.split('::') - frappe.db.sql('''insert into __ListSettings (user, doctype, data) values (%s, %s, %s) - on duplicate key update data=%s''', (user, doctype, data, data)) \ No newline at end of file diff --git a/frappe/model/utils/user_settings.py b/frappe/model/utils/user_settings.py new file mode 100644 index 0000000000..dd58c4b9a4 --- /dev/null +++ b/frappe/model/utils/user_settings.py @@ -0,0 +1,48 @@ +# Settings saved per user basis +# such as page_limit, filters, last_view + +import frappe, json + +def get_user_settings(doctype, for_update=False): + user_settings = frappe.cache().hget('_user_settings', + '{0}::{1}'.format(doctype, frappe.session.user)) + + if user_settings is None: + user_settings = frappe.db.sql('''select data from __UserSettings + where user=%s and doctype=%s''', (frappe.session.user, doctype)) + user_settings = user_settings and user_settings[0][0] or '{}' + + if not for_update: + update_user_settings(doctype, user_settings, True) + + return user_settings or '{}' + +def update_user_settings(doctype, user_settings, for_update=False): + '''update user settings in cache''' + + if for_update: + current = json.loads(user_settings) + else: + current = json.loads(get_user_settings(doctype, for_update = True)) + + if isinstance(current, basestring): + # corrupt due to old code, remove this in a future release + current = {} + + current.update(user_settings) + + frappe.cache().hset('_user_settings', '{0}::{1}'.format(doctype, frappe.session.user), + json.dumps(current)) + +def sync_user_settings(): + '''Sync from cache to database (called asynchronously via the browser)''' + for key, data in frappe.cache().hgetall('_user_settings').iteritems(): + doctype, user = key.split('::') + frappe.db.sql('''insert into __UserSettings (user, doctype, data) values (%s, %s, %s) + on duplicate key update data=%s''', (user, doctype, data, data)) + +@frappe.whitelist() +def save(doctype, user_settings): + user_settings = json.loads(user_settings or '{}') + update_user_settings(doctype, user_settings) + return user_settings diff --git a/frappe/patches.txt b/frappe/patches.txt index c7f7e06f98..e1e0a2dad4 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -163,5 +163,5 @@ execute:frappe.rename_doc('Country', 'Macedonia, Republic of', 'Macedonia', igno execute:frappe.rename_doc('Country', 'Iran, Islamic Republic of', 'Iran', ignore_if_exists=True) execute:frappe.rename_doc('Country', 'Tanzania, United Republic of', 'Tanzania', ignore_if_exists=True) execute:frappe.rename_doc('Country', 'Syrian Arab Republic', 'Syria', ignore_if_exists=True) +frappe.patches.v8_0.rename_listsettings_to_usersettings frappe.patches.v7_2.update_communications - diff --git a/frappe/patches/v8_0/__init__.py b/frappe/patches/v8_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v8_0/rename_listsettings_to_usersettings.py b/frappe/patches/v8_0/rename_listsettings_to_usersettings.py new file mode 100644 index 0000000000..0db09f9dc2 --- /dev/null +++ b/frappe/patches/v8_0/rename_listsettings_to_usersettings.py @@ -0,0 +1,26 @@ +import frappe, json + +def execute(): + for us in frappe.db.sql('''select user, doctype, data from __ListSettings''', as_dict=True): + try: + data = json.loads(us.data) + except: + continue + + if 'List' in data: + continue + + if 'limit' in data: + data['page_length'] = data['limit'] + del data['limit'] + + new_data = dict(List=data) + new_data = json.dumps(new_data) + + frappe.db.sql('''update __ListSettings + set data=%(new_data)s + where user=%(user)s + and doctype=%(doctype)s''', + {'new_data': new_data, 'user': us.user, 'doctype': us.doctype}) + + frappe.db.sql("RENAME TABLE __ListSettings to __UserSettings") \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index 2f62b67a8d..86f648fa76 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -112,6 +112,7 @@ "public/js/frappe/model/create_new.js", "public/js/frappe/model/perm.js", "public/js/frappe/model/workflow.js", + "public/js/frappe/model/user_settings.js", "public/js/lib/md5.min.js", "public/js/frappe/misc/user.js", @@ -211,7 +212,9 @@ ], "js/list.min.js": [ "public/js/frappe/ui/listing.html", - "public/js/frappe/ui/listing.js", + + "public/js/frappe/ui/base_list.js", + "public/js/frappe/model/indicator.js", "public/js/frappe/ui/filters/filters.js", "public/js/frappe/ui/filters/edit_filter.html", @@ -222,7 +225,9 @@ "public/js/frappe/ui/like.js", "public/js/frappe/ui/liked_by.html", "public/html/print_template.html", - "public/js/frappe/list/doclistview.js", + + "public/js/frappe/list/list_view.js", + "public/js/frappe/list/list_sidebar.js", "public/js/frappe/list/list_sidebar.html", "public/js/frappe/list/list_sidebar_stat.html", @@ -232,14 +237,21 @@ "public/js/frappe/list/list_item_row_head.html", "public/js/frappe/list/list_item_subject.html", "public/js/frappe/list/list_permission_footer.html", - "public/js/frappe/list/listview.js", - "public/js/frappe/views/calendar.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/list_renderer.js", + "public/js/frappe/views/gantt/gantt_view.js", + "public/js/frappe/views/calendar/calendar.js", + "public/js/frappe/views/image/image_view.js", + "public/js/frappe/views/kanban/kanban_view.js", + "public/js/frappe/list/header_select_all_like_filter.html", "public/js/frappe/list/item_assigned_to_comment_count.html", "public/js/frappe/views/treeview.js", + + "public/js/frappe/views/image/image_view_item_row.html", + "public/js/frappe/views/image/image_view_item_main_head.html", + "public/js/frappe/views/image/photoswipe_dom.html", + "public/js/frappe/views/kanban/kanban_board.html", "public/js/frappe/views/kanban/kanban_column.html", "public/js/frappe/views/kanban/kanban_card.html" diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index d87fe1b0f5..4bf3cb0275 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -838,3 +838,29 @@ fieldset[disabled] .form-control { .c3-tooltip td.value { text-align: right; } +input[type="checkbox"] { + visibility: hidden; + position: relative; +} +input[type="checkbox"]:before { + position: absolute; + font-family: 'FontAwesome'; + content: '\f096'; + visibility: visible; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 14px; + display: inline-block; + font-size: 14px; + color: #d1d8dd; + -webkit-transition: 150ms color; + -o-transition: 150ms color; + transition: 150ms color; +} +input[type="checkbox"]:checked:before { + content: '\f14a'; + font-size: 13px; + color: #3b99fc; +} diff --git a/frappe/public/css/form_grid.css b/frappe/public/css/form_grid.css index b0614d1205..2b20fd89e9 100644 --- a/frappe/public/css/form_grid.css +++ b/frappe/public/css/form_grid.css @@ -76,6 +76,7 @@ .grid-body .editable-row .checkbox { margin: 0px; text-align: center; + margin-top: 9px; } .grid-body .editable-row textarea { height: 38px !important; diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 05e57a0674..fe52ee026c 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -110,6 +110,25 @@ border-top: 1px solid #d1d8dd; } .list-value { + display: table; + vertical-align: middle; +} +.list-value .list-row-checkbox, +.list-value .liked-by, +.list-value .list-id, +.list-value .list-select-all { + display: table-cell; + vertical-align: middle; +} +.list-value .list-row-checkbox, +.list-value .list-select-all { + margin: 0; + margin-right: 7px; +} +.list-value .liked-by { + padding-top: 2px; +} +.list-value .list-col-title { vertical-align: middle; } .progress { @@ -118,7 +137,7 @@ .doclist-row { font-size: 12px; } -.doclist-row .likes-count { +.likes-count { display: inline-block; width: 15px; margin-left: -5px; @@ -164,9 +183,6 @@ .listview-main-section .octicon-heart { cursor: pointer; } -.list-row-head .octicon-heart { - margin-right: 13px; -} .like-action.octicon-heart { color: #ff5858; } @@ -198,55 +214,6 @@ width: 37px; text-align: left; } -.image-view { - float: left; -} -.image-view-subject { - padding: inherit; -} -.image-view-col { - padding-bottom: 5px; - padding-top: 5px; -} -.image-view-col a { - text-decoration: none !important; -} -table.field-info { - opacity: 0; - bottom: -20px; - font-size: 8pt; - color: white; - background-color: #36414C; - position: absolute; - padding-bottom: 0px !important; - border: 0px; -} -table.field-info tr td { - border: none !important; -} -.zoom-view { - top: 10px !important; - right: 10px !important; - width: 36px; - height: 36px; - opacity: 0; - font-size: 16px; - color: #36414C; - position: absolute; - padding: 0px !important; - border-radius: 5px; - border: 0px; -} -.image-field { - background-size: contain; - position: relative; -} -.image-field:hover .field-info { - opacity: 0.7; -} -.image-field:hover .zoom-view { - opacity: 0.6; -} .tag-row { margin-top: 5px; padding-left: 55px; @@ -291,3 +258,99 @@ table.field-info tr td { font-size: 11px; max-width: 100px; } +.image-view-container { + display: flex; + flex-wrap: wrap; +} +.image-view-container .image-view-item { + flex: 0 0 25%; + padding: 15px; + border-bottom: 1px solid #EBEFF2; + border-right: 1px solid #EBEFF2; +} +.image-view-container .image-view-item:nth-child(4n) { + border-right: none; +} +.image-view-container .image-view-item:nth-last-child(-n + 4):nth-child(4n + 1), +.image-view-container .image-view-item:nth-last-child(-n + 4):nth-child(4n + 1) ~ .image-view-item { + border-bottom: none; +} +.image-view-container .image-view-header { + margin-bottom: 10px; +} +.image-view-container .image-view-body:hover .zoom-view { + opacity: 0.7; +} +.image-view-container .image-view-body a { + text-decoration: none; +} +.image-view-container .image-field { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + position: relative; + height: 200px; + padding: 15px; +} +.image-view-container .image-field img { + max-height: 100%; +} +.image-view-container .placeholder-text { + font-size: 72px; + color: #d1d8dd; +} +.image-view-container .zoom-view { + bottom: 10px !important; + right: 10px !important; + width: 36px; + height: 36px; + opacity: 0; + font-size: 16px; + color: #36414C; + position: absolute; +} +@media (max-width: 767px) { + .image-view-container .zoom-view { + opacity: 0.5; + } +} +@media (max-width: 991px) { + .image-view-container .image-view-item { + flex: 0 0 33.33333333%; + } + .image-view-container .image-view-item:nth-child(3n) { + border-right: none; + } + .image-view-container .image-view-item:nth-last-child(-n + 3):nth-child(3n + 1), + .image-view-container .image-view-item:nth-last-child(-n + 3):nth-child(3n + 1) ~ .image-view-item { + border-bottom: none; + } + .image-view-container .image-view-item:nth-child(4n) { + border-right: 1px solid #EBEFF2; + } + .image-view-container .image-view-item:nth-last-child(-n + 4):nth-child(4n + 1), + .image-view-container .image-view-item:nth-last-child(-n + 4):nth-child(4n + 1) ~ .image-view-item { + border-bottom: 1px solid #EBEFF2; + } +} +.pswp--svg .pswp__button, +.pswp--svg .pswp__button--arrow--left:before, +.pswp--svg .pswp__button--arrow--right:before { + background-image: url('/assets/frappe/images/default-skin.svg') !important; +} +.pswp--svg .pswp__button--arrow--left, +.pswp--svg .pswp__button--arrow--right { + background: none !important; +} +.pswp__bg { + background-color: #fff !important; +} +.gantt .details-container .heading { + margin-bottom: 10px; + font-size: 12px; +} +.gantt .details-container .avatar-small { + width: 16px; + height: 16px; +} diff --git a/frappe/public/css/sidebar.css b/frappe/public/css/sidebar.css index 1541244de3..5b7fa2e8fd 100644 --- a/frappe/public/css/sidebar.css +++ b/frappe/public/css/sidebar.css @@ -121,6 +121,9 @@ body[data-route^="Module"] .main-menu .form-sidebar { .form-sidebar .form-viewers .shared-with-everyone .octicon { color: #36414C !important; } +.form-sidebar .liked-by { + margin-left: -4px; +} .form-sidebar .liked-by .octicon-heart { font-size: 16px; cursor: pointer; diff --git a/frappe/public/images/default-skin.svg b/frappe/public/images/default-skin.svg new file mode 100755 index 0000000000..9d5f0c6a10 --- /dev/null +++ b/frappe/public/images/default-skin.svg @@ -0,0 +1 @@ +default-skin 2 \ No newline at end of file diff --git a/frappe/public/js/frappe/form/sidebar.js b/frappe/public/js/frappe/form/sidebar.js index 165688ddc8..080e6cd898 100644 --- a/frappe/public/js/frappe/form/sidebar.js +++ b/frappe/public/js/frappe/form/sidebar.js @@ -128,7 +128,7 @@ frappe.ui.form.Sidebar = Class.extend({ make_like: function() { this.like_wrapper = this.sidebar.find(".liked-by"); this.like_icon = this.sidebar.find(".liked-by .octicon-heart"); - this.like_count = this.sidebar.find(".liked-by .like-count"); + this.like_count = this.sidebar.find(".liked-by .likes-count"); frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by"); }, diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index cf5454b56c..e44a55cedc 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -67,8 +67,8 @@
{% } %} {% } %} diff --git a/frappe/public/js/frappe/list/list_item_row.html b/frappe/public/js/frappe/list/list_item_row.html index 1995a3f931..0db51d0a25 100644 --- a/frappe/public/js/frappe/list/list_item_row.html +++ b/frappe/public/js/frappe/list/list_item_row.html @@ -1,6 +1,6 @@
{% if (is_different) { %} @@ -24,7 +24,7 @@
-
{%= list.get_indicator_dot(data) %}
+
{%= indicator_dot %}
{%= frappe.render_template("item_assigned_to_comment_count", { data: data }) %}
diff --git a/frappe/public/js/frappe/list/list_item_row_head.html b/frappe/public/js/frappe/list/list_item_row_head.html index c5b286adfd..56de9b684a 100644 --- a/frappe/public/js/frappe/list/list_item_row_head.html +++ b/frappe/public/js/frappe/list/list_item_row_head.html @@ -1,4 +1,4 @@ -
+
+ {% } %} {{ strip_html(_title) }} {% if (_workflow && !_without_workflow) { %} diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js new file mode 100644 index 0000000000..59f9d6f91c --- /dev/null +++ b/frappe/public/js/frappe/list/list_renderer.js @@ -0,0 +1,528 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.provide('frappe.views'); + +// Renders customized list +// usually based on `in_list_view` property + +frappe.views.ListRenderer = Class.extend({ + name: 'List', + init: function (opts) { + $.extend(this, opts); + this.meta = frappe.get_meta(this.doctype); + + this.init_settings(); + this.set_defaults(); + this.set_fields(); + this.set_columns(); + this.setup_cache(); + }, + set_defaults: function () { + var me = this; + this.page_title = __(this.doctype); + + this.set_wrapper(); + this.prepare_render_view(); + + // flag to enable/disable realtime updates in list_view + this.no_realtime = false; + + // set false to render view even if no results + // e.g Calendar + this.show_no_result = true; + + // hide sort selector + this.hide_sort_selector = false; + + // default settings + this.order_by = this.order_by || 'modified desc'; + this.filters = this.filters || []; + this.page_length = this.page_length || 20; + }, + setup_cache: function () { + frappe.provide('frappe.views.list_renderers.' + this.doctype); + frappe.views.list_renderers[this.doctype][this.list_view.current_view] = this; + }, + init_settings: function () { + this.settings = frappe.listview_settings[this.doctype] || {}; + this.init_user_settings(); + + this.order_by = this.user_settings.order_by || this.settings.order_by; + this.filters = this.user_settings.filters || this.settings.filters; + this.page_length = this.user_settings.page_length || this.settings.page_length; + + // default filter for submittable doctype + if(frappe.model.is_submittable(this.doctype) && (!this.filters || !this.filters.length)) { + this.filters = [[this.doctype, "docstatus", "!=", 2]]; + } + }, + init_user_settings: function () { + frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.name); + this.user_settings = frappe.get_user_settings(this.doctype)[this.name]; + }, + after_refresh: function() { + // called after refresh in list_view + }, + before_refresh: function() { + // called before refresh in list_view + }, + should_refresh: function() { + return this.list_view.current_view !== this.list_view.last_view; + }, + set_wrapper: function () { + this.wrapper = this.list_view.wrapper.find('.result-list'); + }, + set_fields: function () { + var me = this; + var tabDoctype = '`tab' + this.doctype + '`.'; + this.fields = []; + this.stats = ['_user_tags']; + + var add_field = function (fieldname) { + if (!fieldname.includes('`tab')) { + fieldname = tabDoctype + '`' + fieldname + '`'; + } + if (!me.fields.includes(fieldname)) + me.fields.push(fieldname); + } + + var defaults = [ + 'name', + 'owner', + 'docstatus', + '_user_tags', + '_comments', + 'modified', + 'modified_by', + '_assign', + '_liked_by', + '_seen' + ]; + defaults.map(add_field); + + // add title field + if (this.meta.title_field) { + this.title_field = this.meta.title_field; + add_field(this.meta.title_field); + } + + // enabled / disabled + if (frappe.meta.has_field(this.doctype, 'enabled')) { add_field('enabled'); }; + if (frappe.meta.has_field(this.doctype, 'disabled')) { add_field('disabled'); }; + + // add workflow field (as priority) + this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype); + if (this.workflow_state_fieldname) { + if (!frappe.workflow.workflows[this.doctype]['override_status']) { + add_field(this.workflow_state_fieldname); + } + this.stats.push(this.workflow_state_fieldname); + } + + this.meta.fields.forEach(function (df, i) { + if (df.in_list_view && frappe.perm.has_perm(me.doctype, df.permlevel, 'read')) { + if (df.fieldtype == 'Image' && df.options) { + add_field(df.options); + } else { + add_field(df.fieldname); + } + // currency field for symbol (multi-currency) + if (df.fieldtype == 'Currency' && df.options) { + if (df.options.includes(':')) { + add_field(df.options.split(':')[1]); + } else { + add_field(df.options); + }; + } + } + }); + + // additional fields + if (this.settings.add_fields) { + this.settings.add_fields.forEach(add_field); + } + // kanban column fields + if (me.meta.__kanban_column_fields) { + me.fields = me.fields.concat(me.meta.__kanban_column_fields); + } + }, + set_columns: function () { + var me = this; + this.columns = []; + var name_column = { + colspan: this.settings.colwidths && this.settings.colwidths.subject || 6, + type: 'Subject', + title: 'Name' + }; + if (this.meta.title_field) { + name_column.title = frappe.meta.get_docfield(this.doctype, this.meta.title_field).label; + } + this.columns.push(name_column); + + if (frappe.has_indicator(this.doctype)) { + // indicator + this.columns.push({ + colspan: this.settings.colwidths && this.settings.colwidths.indicator || 3, + type: 'Indicator', + title: 'Status' + }); + } + + // total_colspans + this.total_colspans = this.columns.reduce(function (total, curr) { + return total + curr.colspan; + }, 0); + + // overridden + var overridden = (this.settings.add_columns || []).map(function (d) { + return d.content; + }); + + // custom fields in list_view + var docfields_in_list_view = + frappe.get_children('DocType', this.doctype, 'fields', { 'in_list_view': 1 }) + .sort(function (a, b) { + return a.idx - b.idx + }); + + docfields_in_list_view.forEach(function (d) { + if (overridden.includes(d.fieldname) || d.fieldname === me.title_field) { + return; + } + if (me.total_colspans < 12) { + me.add_column(d); + } + }); + + // additional columns + if (this.settings.add_columns) { + this.settings.add_columns.forEach(function (d) { + if (me.total_colspans < 12) { + if (typeof d === 'string') { + me.add_column(frappe.meta.get_docfield(me.doctype, d)); + } else { + me.columns.push(d); + me.total_colspans += parseInt(d.colspan); + } + } + }); + } + + + // distribute remaining columns + var empty_cols = flt(12 - this.total_colspans); + while (empty_cols > 0) { + this.columns = this.columns.map(function (col) { + if (empty_cols > 0) { + col.colspan = cint(col.colspan) + 1; + empty_cols = empty_cols - 1; + } + return col; + }); + } + }, + add_column: function (df) { + // field width + var colspan = 3; + if (in_list(['Int', 'Percent'], df.fieldtype)) { + colspan = 2; + } else if (in_list(['Check', 'Image'], df.fieldtype)) { + colspan = 1; + } else if (in_list(['name', 'subject', 'title'], df.fieldname)) { + // subjects are longer + colspan = 4; + } else if (df.fieldtype == 'Text Editor' || df.fieldtype == 'Text') { + colspan = 4; + } + + if (df.columns && df.columns > 0) { + colspan = df.columns; + } else if (this.settings.column_colspan && this.settings.column_colspan[df.fieldname]) { + colspan = this.settings.column_colspan[df.fieldname]; + } else { + colspan = 2; + } + this.total_colspans += parseInt(colspan); + var col = { + colspan: colspan, + content: df.fieldname, + type: df.fieldtype, + df: df, + fieldtype: df.fieldtype, + fieldname: df.fieldname, + title: __(df.label) + }; + if (this.settings.column_render && this.settings.column_render[df.fieldname]) { + col.render = this.settings.column_render[df.fieldname]; + } + this.columns.push(col); + }, + + setup_filterable: function () { + var me = this; + this.wrapper.on('click', '.filterable', function (e) { + var filters = $(this).attr('data-filter').split('|'); + var added = false; + + filters.forEach(function (f) { + f = f.split(','); + if (f[2] === 'Today') { + f[2] = frappe.datetime.get_today(); + } else if (f[2] == 'User') { + f[2] = frappe.session.user; + } + var new_filter = me.list_view.filter_list + .add_filter(me.doctype, f[0], f[1], f.slice(2).join(',')); + + if (new_filter) { + // set it to true if atleast 1 filter is added + added = true; + } + }); + if (added) { + me.list_view.refresh(true); + } + }); + this.wrapper.on('click', '.list-row-left', function (e) { + // don't open in case of checkbox, like, filterable + if ($(e.target).hasClass('filterable') + || $(e.target).hasClass('octicon-heart') + || $(e.target).is(':checkbox')) { + return; + } + + var link = $(this).parent().find('a.list-id').get(0); + window.location.href = link.href; + return false; + }); + }, + + render_view: function (values) { + var me = this; + values.map(function (value) { + var row = $('
') + .data("data", me.meta) + .appendTo(me.wrapper).get(0); + me.render_item(row, value); + }); + + this.setup_filterable(); + }, + + // renders data(doc) in element + render_item: function (element, data) { + + $(element).append(this.get_item_html(data)); + + if (this.settings.post_render_item) { + this.settings.post_render_item(this, element, data); + } + }, + + // returns html for a data item, + // usually based on a template + get_item_html: function (data) { + var main = frappe.render_template('list_item_main', { + data: data, + columns: this.columns, + formatters: this.settings.formatters, + subject: this.get_subject_html(data, true), + indicator: this.get_indicator_html(data), + right_column: this.settings.right_column + }); + + return frappe.render_template('list_item_row', { + data: data, + main: main, + settings: this.settings, + meta: this.meta, + indicator_dot: this.get_indicator_dot(data), + right_column: this.settings.right_column + }) + }, + + get_header_html: function () { + var main = frappe.render_template('list_item_main_head', { + columns: this.columns, + right_column: this.settings.right_column, + _checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) + && !this.no_delete) + }); + + return frappe.render_template('list_item_row_head', { main: main, list: this }); + }, + + render_tags: function (element, data) { + var me = this; + var tag_row = $(`
+ +
+
`).appendTo(element); + + if (!me.list_view.tags_shown) { + tag_row.addClass('hide'); + } + + // add tags + var tag_editor = new frappe.ui.TagEditor({ + parent: tag_row.find('.list-tag'), + frm: { + doctype: this.doctype, + docname: data.name + }, + list_sidebar: me.list_view.list_sidebar, + user_tags: data._user_tags, + on_change: function (user_tags) { + data._user_tags = user_tags; + } + }); + tag_editor.wrapper.on('click', '.tagit-label', function () { + me.list_view.set_filter('_user_tags', $(this).text()); + }); + }, + + get_subject_html: function (data, without_workflow) { + data._without_workflow = without_workflow; + return frappe.render_template('list_item_subject', data); + }, + + get_indicator_html: function (doc) { + var indicator = frappe.get_indicator(doc, this.doctype); + if (indicator) { + return ` + ${__(indicator[0])} + `; + } + return ''; + }, + + get_indicator_dot: function (doc) { + var indicator = frappe.get_indicator(doc, this.doctype); + if (!indicator) { + return ''; + } + return ``; + }, + + prepare_data: function (data) { + if (data.modified) + this.prepare_when(data, data.modified); + + // nulls as strings + for (key in data) { + if (data[key] == null) { + data[key] = ''; + } + } + + data.doctype = this.doctype; + data._liked_by = JSON.parse(data._liked_by || '[]'); + data._checkbox = (frappe.model.can_delete(this.doctype) || this.settings.selectable) && !this.no_delete + + data._doctype_encoded = encodeURIComponent(data.doctype); + data._name = data.name.replace(/'/g, '\''); + data._name_encoded = encodeURIComponent(data.name); + data._submittable = frappe.model.is_submittable(this.doctype); + + var title_field = frappe.get_meta(this.doctype).title_field || 'name'; + data._title = strip_html(data[title_field]) || data.name; + data._full_title = data._title; + + if (data._title.length > 35) { + data._title = data._title.slice(0, 35) + '...'; + } + + data._workflow = null; + if (this.workflow_state_fieldname) { + data._workflow = { + fieldname: this.workflow_state_fieldname, + value: data[this.workflow_state_fieldname], + style: frappe.utils.guess_style(data[this.workflow_state_fieldname]) + } + } + + data._user = frappe.session.user; + + data._tags = data._user_tags.split(',').filter(function (v) { + // filter falsy values + return v; + }); + + data.css_seen = ''; + if (data._seen) { + var seen = JSON.parse(data._seen); + if (seen && in_list(seen, data._user)) { + data.css_seen = 'seen' + } + } + + data._assign_list = JSON.parse(data._assign || '[]'); + + // prepare data in settings + if (this.settings.prepare_data) + this.settings.prepare_data(data); + + return data; + }, + + prepare_when: function (data, date_str) { + if (!date_str) date_str = data.modified; + // when + data.when = (dateutil.str_to_user(date_str)).split(' ')[0]; + var diff = dateutil.get_diff(dateutil.get_today(), date_str.split(' ')[0]); + if (diff === 0) { + data.when = comment_when(date_str); + } + if (diff === 1) { + data.when = __('Yesterday') + } + if (diff === 2) { + data.when = __('2 days ago') + } + }, + + // for views which require 3rd party libs + required_libs: null, + + prepare_render_view: function () { + var me = this; + this._render_view = this.render_view; + + var lib_exists = (typeof this.required_libs === 'string' && this.required_libs) + || ($.isArray(this.required_libs) && this.required_libs.length); + + this.render_view = function (values) { + // prepare data before rendering view + values = values.map(me.prepare_data.bind(this)); + + if (lib_exists) { + me.load_lib(function () { + me._render_view(values); + }); + } else { + me._render_view(values); + } + }.bind(this); + }, + + load_lib: function (callback) { + frappe.require(this.required_libs, callback); + }, + + render_bar_graph: function (parent, data, field, label) { + var args = { + percent: data[field], + label: __(label) + } + $(parent).append(` \ + \ + `); + }, + render_icon: function (parent, icon_class, label) { + var icon_html = ``; + $(parent).append(icon_html); + } +}); diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 59522472c4..d1431adc97 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -17,7 +17,7 @@ frappe.views.ListSidebar = Class.extend({ this.cat_tags = []; }, make: function() { - var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.doclistview.doctype}); + var sidebar_content = frappe.render_template("list_sidebar", {doctype: this.list_view.doctype}); this.sidebar = $('') .html(sidebar_content) @@ -63,7 +63,7 @@ frappe.views.ListSidebar = Class.extend({ .attr('disabled', null).removeClass('disabled') // show image link if image_view - if(this.doclistview.meta.image_field) { + if(this.list_view.meta.image_field) { this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide'); show_list_link = true; } @@ -103,8 +103,8 @@ frappe.views.ListSidebar = Class.extend({ } // from reference doctype - if(this.doclistview.listview.settings.reports) { - add_reports(this.doclistview.listview.settings.reports) + if(this.list_view.list_renderer.settings.reports) { + add_reports(this.list_view.list_renderer.settings.reports) } // from specially tagged reports @@ -119,12 +119,12 @@ frappe.views.ListSidebar = Class.extend({ var boards = frappe.get_meta(this.doctype).__kanban_boards; if (!boards) return; boards.forEach(function(board) { - var route = ["List", board.parent, "Kanban", board.name].join('/'); + var route = ["List", board.reference_doctype, "Kanban", board.name].join('/'); if(!divider) { $('').appendTo($dropdown); divider = true; } - $('
  • '+board.name+'
  • ').appendTo($dropdown); + $(`
  • ${__(board.name)}
  • `).appendTo($dropdown); }); $dropdown.find('.new-kanban-board').click(function() { @@ -179,10 +179,8 @@ frappe.views.ListSidebar = Class.extend({ me.add_custom_column_field(custom_column) .then(function(custom_column) { - console.log(custom_column) var f = custom_column ? 'kanban_column' : values.field_name; - console.log(f) return me.make_kanban_board(values.board_name, f) }) .then(function() { @@ -239,7 +237,7 @@ frappe.views.ListSidebar = Class.extend({ setup_assigned_to_me: function() { var me = this; this.page.sidebar.find(".assigned-to-me a").on("click", function() { - me.doclistview.assigned_to_me(); + me.list_view.assigned_to_me(); }); }, get_cat_tags:function(){ @@ -280,7 +278,7 @@ frappe.views.ListSidebar = Class.extend({ //render normal stats me.render_stat("_user_tags", (r.message.stats|| {})["_user_tags"]); } - me.doclistview.set_sidebar_height(); + me.list_view.set_sidebar_height(); } }); }, @@ -333,8 +331,8 @@ frappe.views.ListSidebar = Class.extend({ var fieldname = $(this).attr('data-field'); var label = $(this).attr('data-label'); if (label == "No Tags") { - me.doclistview.filter_list.add_filter(me.doclistview.doctype, fieldname, 'not like', '%,%') - me.doclistview.run(); + me.list_view.filter_list.add_filter(me.list_view.doctype, fieldname, 'not like', '%,%') + me.list_view.run(); } else { me.set_filter(fieldname, label); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js new file mode 100644 index 0000000000..912c15af64 --- /dev/null +++ b/frappe/public/js/frappe/list/list_view.js @@ -0,0 +1,853 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.provide('frappe.views.list_view'); +frappe.provide('frappe.views.list_renderers'); + +cur_list = null; +frappe.views.ListFactory = frappe.views.Factory.extend({ + make: function (route) { + var me = this; + var doctype = route[1]; + + frappe.model.with_doctype(doctype, function () { + if (locals['DocType'][doctype].issingle) { + frappe.set_re_route('Form', doctype); + } else { + if (!frappe.views.list_view[doctype]) { + frappe.views.list_view[doctype] = new frappe.views.ListView({ + doctype: doctype, + parent: me.make_page(true, 'List/' + doctype) + }); + } else { + frappe.container.change_to(frappe.views.list_view[doctype].page_name); + } + me.set_cur_list(); + } + }); + }, + show: function () { + this.set_module_breadcrumb(); + this._super(); + this.set_cur_list(); + cur_list && cur_list.refresh(); + }, + set_module_breadcrumb: function () { + if (frappe.route_history.length > 1) { + var prev_route = frappe.route_history[frappe.route_history.length - 2]; + if (prev_route[0] === 'modules') { + var doctype = frappe.get_route()[1], + module = prev_route[1]; + if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) { + // save the last page from the breadcrumb was accessed + frappe.breadcrumbs.set_doctype_module(doctype, module); + } + } + } + }, + set_cur_list: function () { + var route = frappe.get_route(); + cur_list = frappe.container.page && frappe.container.page.list_view; + if (cur_list && cur_list.doctype !== route[1]) { + // changing... + cur_list = null; + } + } +}); + +$(document).on('save', function (event, doc) { + frappe.views.set_list_as_dirty(doc.doctype); +}); + +frappe.views.set_list_as_dirty = function (doctype) { + if (frappe.views.trees[doctype]) { + frappe.views.trees[doctype].tree.refresh(); + } + + var route = frappe.get_route(); + var current_view = route[2] || 'List'; + + var list_renderer = frappe.views.list_renderers[doctype]; + if (list_renderer + && list_renderer[current_view] + && list_renderer[current_view].no_realtime) { + return; + } + + var list_page = 'List/' + doctype; + if (frappe.pages[list_page]) { + if (frappe.pages[list_page].list_view) { + if (frappe.pages[list_page].list_view.dirty) { + // already refreshing... + return; + } + frappe.pages[list_page].list_view.dirty = true; + } + } + if (route[0] === 'List' && route[1] === doctype) { + setTimeout(function () { + frappe.pages[list_page].list_view.refresh(); + }, 100); + } +} + +frappe.views.ListView = frappe.ui.BaseList.extend({ + init: function (opts) { + $.extend(this, opts); + + if (!frappe.boot.user.all_read.includes(this.doctype)) { + frappe.show_not_permitted(frappe.get_route_str()); + return; + } + + this.page_name = 'List/' + this.doctype; + this.dirty = true; + this.tags_shown = false; + + this.page_title = __(this.doctype); + this.page_title = + (this.page_title.toLowerCase().substr(-4) == 'list') && __(this.page_title) + || __(this.page_title) + ' ' + __('List'); + + this.make_page(); + this.setup(); + + // refresh on init + this.refresh(); + }, + + make_page: function () { + this.parent.list_view = this; + this.page = this.parent.page; + + this.$page = $(this.parent).css({ 'min-height': '400px' }); + + $(`
    `) + .appendTo(this.page.main); + + this.page.main.addClass('listview-main-section'); + var module = locals.DocType[this.doctype].module; + + frappe.breadcrumbs.add(module, this.doctype); + }, + + setup: function () { + this.can_delete = frappe.model.can_delete(this.doctype); + this.meta = frappe.get_meta(this.doctype); + this.wrapper = this.$page.find('.frappe-list-area').empty(); + this.allow_delete = true; + + this.load_last_view(); + this.setup_view_variables(); + + this.setup_list_renderer(); + this.init_base_list(false); + this.list_renderer.set_wrapper(); + this.list_renderer_onload(); + + this.show_match_help(); + this.init_menu(); + this.init_sort_selector(); + this.init_filters(); + this.set_title(); + this.init_headers(); + }, + + refresh_surroundings: function() { + this.init_sort_selector(); + this.init_filters(); + this.set_title(); + this.init_headers(); + }, + + setup_list_renderer: function () { + frappe.provide('frappe.views.list_renderers.' + this.doctype); + + var list_renderer = frappe.views.list_renderers[this.doctype][this.current_view]; + if (list_renderer) { + this.list_renderer = list_renderer; + this.list_renderer.init_settings(); + return; + } + + var opts = { + doctype: this.doctype, + list_view: this + } + + if (this.current_view === 'List') { + this.list_renderer = new frappe.views.ListRenderer(opts); + } else if (this.current_view === 'Gantt') { + this.list_renderer = new frappe.views.GanttView(opts); + } else if (this.current_view === 'Calendar') { + this.list_renderer = new frappe.views.CalendarView(opts); + } else if (this.current_view === 'Image') { + this.list_renderer = new frappe.views.ImageView(opts); + } else if (this.current_view === 'Kanban') { + this.list_renderer = new frappe.views.KanbanView(opts); + } + }, + + render_view: function (values) { + this.list_renderer.render_view(values); + }, + + set_title: function () { + if (this.list_renderer.page_title) { + this.page.set_title(this.list_renderer.page_title); + } else { + this.page.set_title(this.page_title); + } + }, + + load_last_view: function () { + var us = frappe.get_user_settings(this.doctype); + var route = ['List', this.doctype]; + + if (us.last_view && us.last_view !== 'List') { + route.push(us.last_view); + + if (us.last_view === 'Kanban') { + route.push(us['Kanban'].last_kanban_board); + } + } + + frappe.set_route(route); + }, + + init_headers: function () { + this.page.main.find('.list-headers > .list-row-head').hide(); + this.list_header = this.page.main.find('.list-headers > ' + + '.list-row-head[data-list-renderer="' + + this.list_renderer.name +'"]'); + + if(this.list_header.length > 0) { + this.list_header.show(); + return; + } + + + var html = this.list_renderer.get_header_html(); + if(!html) { + this.list_header = $(); + return; + } + + this.list_header = $(html).appendTo(this.page.main.find('.list-headers')); + + this.setup_like(); + this.setup_select_all(); + this.setup_delete(); + }, + + list_renderer_onload: function () { + if (this.list_renderer.settings.onload) { + this.list_renderer.settings.onload(this); + } + }, + + set_sidebar_height: function () { + var h_main = this.page.sidebar.height(); + var h_side = this.$page.find('.layout-side-section').height(); + if (h_side > h_main) + this.$page.find('.layout-main-section').css({ 'min-height': h_side }); + }, + + init_filters: function () { + this.filter_list = new frappe.ui.FilterList({ + base_list: this, + parent: this.wrapper.find('.list-filters').show(), + doctype: this.doctype, + default_filters: [] + }); + this.set_filters(this.list_renderer.filters); + }, + + set_filters: function (filters) { + var me = this; + $.each(filters, function (i, f) { + if (f.length === 3) { + f = [me.doctype, f[0], f[1], f[2]] + } + me.filter_list.add_filter(f[0], f[1], f[2], f[3]); + }); + }, + + init_sort_selector: function () { + var me = this; + var order_by = this.list_renderer.order_by; + + this.sort_selector = new frappe.ui.SortSelector({ + parent: this.wrapper.find('.list-filters'), + doctype: this.doctype, + args: order_by, + change: function () { me.run(); } + }); + }, + + show_match_help: function () { + var me = this; + var match_rules_list = frappe.perm.get_match_rules(this.doctype); + var perm = frappe.perm.get_perm(this.doctype); + + if (match_rules_list.length) { + this.footnote_area = + frappe.utils.set_footnote(this.footnote_area, this.$page.find('.layout-main-section'), + frappe.render_template('list_permission_footer', { + condition_list: match_rules_list + })); + $(this.footnote_area).css({ 'margin-top': '0px' }); + } + }, + + setup_view_variables: function () { + var route = frappe.get_route(); + this.last_view = this.current_view || ''; + this.current_view = route[2] || 'List'; + }, + + init_base_list: function (auto_run) { + var me = this; + // init list + this.make({ + method: 'frappe.desk.reportview.get', + save_user_settings: true, + get_args: this.get_args, + parent: this.wrapper, + freeze: true, + start: 0, + page_length: this.list_renderer.page_length, + show_filters: false, + new_doctype: this.doctype, + no_result_message: this.make_no_result(), + show_no_result: function() { + return me.list_renderer.show_no_result; + } + }); + + this.setup_make_new_doc(); + + if (auto_run !== false && auto_run !== 0) + this.refresh(); + }, + + setup_make_new_doc: function () { + var me = this; + // make_new_doc can be overridden so that default values can be prefilled + // for example - communication list in customer + if (this.list_renderer.settings.list_view_doc) { + this.list_renderer.settings.list_view_doc(this); + } else { + $(this.wrapper).on('click', `button[list_view_doc='${this.doctype}']`, function () { + me.make_new_doc.apply(me, [me.doctype]); + }); + } + }, + + refresh: function (dirty) { + var me = this; + + if (dirty !== undefined) this.dirty = dirty; + + this.refresh_sidebar(); + this.setup_view_variables(); + + if (this.list_renderer.should_refresh()) { + this.setup_list_renderer(); + this.refresh_surroundings(); + this.dirty = true; + } + + if (this.list_renderer.settings.refresh) { + this.list_renderer.settings.refresh(this); + } + + this.set_filters_before_run(); + this.execute_run(); + }, + + execute_run: function () { + if (this.dirty) { + this.run(); + if (this.clean_dash != true) { + this.filter_list.reload_stats(); + } + } else { + if (new Date() - (this.last_updated_on || 0) > 30000) { + // older than 5 mins, refresh + this.run(); + } + } + }, + + save_user_settings_locally: function (args) { + + frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.list_renderer.name); + var user_settings_common = frappe.model.user_settings[this.doctype]; + var user_settings = frappe.model.user_settings[this.doctype][this.list_renderer.name]; + + if (!user_settings) return; + + var different = false; + if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) { + // settings are dirty if filters change + user_settings.filters = args.filters; + different = true; + } + + if (user_settings.order_by !== args.order_by) { + user_settings.order_by = args.order_by; + different = true; + } + + if (user_settings.page_length !== args.page_length) { + user_settings.page_length = args.page_length || 20 + different = true; + } + + // save fields in list settings + if (args.save_user_settings_fields) { + user_settings.fields = args.fields; + } + + // save last view + if (user_settings_common.last_view !== this.current_view) { + user_settings_common.last_view = this.current_view; + different = true; + } + + if (different) { + user_settings_common.updated_on = moment().toString(); + } + }, + + set_filters_before_run: function () { + // set filters from frappe.route_options + // before switching pages, frappe.route_options can have pre-set filters + // for the list view + var me = this; + + if (frappe.route_options) { + this.set_filters_from_route_options(); + this.dirty = true; + } + }, + + run: function (more) { + // set filter from route + var me = this; + + if (this.fresh && !more) { + return; + } + + if (this.list_renderer.settings.before_run) { + this.list_renderer.settings.before_run(this); + } + + if (!this.list_renderer.settings.use_route) { + var route = frappe.get_route(); + if (route[2] && !in_list(['Image', 'Gantt', 'Kanban', 'Calendar'], route[2])) { + $.each(frappe.utils.get_args_dict_from_url(route[2]), function (key, val) { + me.set_filter(key, val, true); + }); + } + } + + this.list_header.find('.list-liked-by-me') + .toggleClass('text-extra-muted not-liked', !this.is_star_filtered()); + + this.last_updated_on = new Date(); + this.dirty = false; + this.clean_dash = false; + // set a fresh so that multiple refreshes do not happen + // at the same time. This is true when deleting. + // AJAX response will try to refresh and list_update notification + // via async will also try to update. + // It is not possible to guess which will reach first + // (most probably async will) but this is a forced way + // to prevent instant refreshes on mutilple triggers + // in a loosly coupled way. + this.fresh = true; + setTimeout(function () { + me.fresh = false; + }, 1000); + + this._super(more); + + if (this.list_renderer.settings.post_render) { + this.list_renderer.settings.post_render(this); + } + + this.wrapper.on('render-complete', function() { + me.list_renderer.after_refresh(); + }) + }, + + make_no_result: function () { + var new_button = frappe.boot.user.can_create.includes(this.doctype) + ? (`

    `) + : ''; + var no_result_message = + `
    +

    ${__('No {0} found', [__(this.doctype)])}

    + ${new_button} +
    `; + + return no_result_message; + }, + + get_args: function () { + var args = { + doctype: this.doctype, + fields: this.list_renderer.fields, + filters: this.get_filters_args(), + order_by: this.get_order_by_args(), + with_comment_count: true + } + return args; + }, + get_filters_args: function() { + var filters = []; + if(this.filter_list) { + // get filters from filter_list + filters = this.filter_list.get_filters(); + } else { + filters = this.list_renderer.filters; + } + // remove duplicates + var uniq = filters.uniqBy(JSON.stringify); + return uniq; + }, + get_order_by_args: function() { + var order_by = ''; + if(this.sort_selector) { + // get order_by from sort_selector + order_by = this.sort_selector.sort_by + ' ' + this.sort_selector.sort_order; + } else { + order_by = this.list_renderer.order_by; + } + return order_by; + }, + assigned_to_me: function () { + this.filter_list.add_filter(this.doctype, '_assign', 'like', '%' + user + '%'); + this.run(); + }, + liked_by_me: function () { + this.filter_list.add_filter(this.doctype, '_liked_by', 'like', '%' + user + '%'); + this.run(); + }, + remove_liked_by_me: function () { + this.filter_list.get_filter('_liked_by').remove(); + }, + is_star_filtered: function () { + return this.filter_list.filter_exists(this.doctype, '_liked_by', 'like', '%' + user + '%'); + }, + init_menu: function () { + var me = this; + this.$page.on('click', '.list-tag-preview', function () { me.toggle_tags(); }); + + // Refresh button for large screens + this.page.set_secondary_action(__('Refresh'), function () { + me.refresh(true); + }, 'octicon octicon-sync') + .addClass('hidden-xs'); + + // Refresh button as menu item in small screens + this.page.add_menu_item(__('Refresh'), function () { + me.refresh(true); + }, 'octicon octicon-sync') + .addClass('visible-xs'); + + if (frappe.model.can_import(this.doctype)) { + this.page.add_menu_item(__('Import'), function () { + frappe.set_route('data-import-tool', { + doctype: me.doctype + }); + }, true); + } + if (frappe.model.can_set_user_permissions(this.doctype)) { + this.page.add_menu_item(__('User Permissions Manager'), function () { + frappe.set_route('user-permissions', { + doctype: me.doctype + }); + }, true); + } + if (user_roles.includes('System Manager')) { + this.page.add_menu_item(__('Role Permissions Manager'), function () { + frappe.set_route('permission-manager', { + doctype: me.doctype + }); + }, true); + this.page.add_menu_item(__('Customize'), function () { + frappe.set_route('Form', 'Customize Form', { + doc_type: me.doctype + }) + }, true); + } + + this.make_bulk_assignment(); + this.make_bulk_printing(); + + // add to desktop + this.page.add_menu_item(__('Add to Desktop'), function () { + frappe.add_to_desktop(me.doctype, me.doctype); + }, true); + + if (user_roles.includes('System Manager') && frappe.boot.developer_mode === 1) { + // edit doctype + this.page.add_menu_item(__('Edit DocType'), function () { + frappe.set_route('Form', 'DocType', me.doctype); + }, true); + } + + }, + make_bulk_assignment: function () { + + var me = this; + + //bulk assignment + me.page.add_menu_item(__('Assign To'), function () { + + var docnames = me.get_checked_items().map(function (doc) { + return doc.name; + }); + + if (docnames.length >= 1) { + me.dialog = new frappe.ui.form.AssignToDialog({ + obj: me, + method: 'frappe.desk.form.assign_to.add_multiple', + doctype: me.doctype, + docname: docnames, + bulk_assign: true, + re_assign: true, + callback: function () { + me.refresh(true); + } + }); + me.dialog.clear(); + me.dialog.show(); + } + else { + frappe.msgprint(__('Select records for assignment')) + } + }, true); + + }, + make_bulk_printing: function () { + var me = this; + var print_settings = frappe.model.get_doc(':Print Settings', 'Print Settings') + var allow_print_for_draft = cint(print_settings.allow_print_for_draft) + var is_submittable = frappe.model.is_submittable(me.doctype) + var allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled) + + //bulk priting + me.page.add_menu_item(__('Print'), function () { + var items = me.get_checked_items(); + + var valid_docs = + items.filter(function (doc) { + return !is_submittable || doc.docstatus === 1 || + (allow_print_for_cancelled && doc.docstatus == 2) || + (allow_print_for_draft && doc.docstatus == 0) || + user_roles.includes('Administrator') + }).map(function (doc) { + return doc.name + }); + + var invalid_docs = items.filter(function (doc) { + return !valid_docs.includes(doc.name); + }); + + if (invalid_docs.length >= 1) { + frappe.msgprint('You selected Draft or Cancelled documents') + return; + } + + if (valid_docs.length >= 1) { + + var dialog = new frappe.ui.Dialog({ + title: 'Print Documents', + fields: [ + { 'fieldtype': 'Check', 'label': __('With Letterhead'), 'fieldname': 'with_letterhead' }, + { 'fieldtype': 'Select', 'label': __('Print Format'), 'fieldname': 'print_sel' }, + ] + }); + + dialog.set_primary_action(__('Print'), function () { + args = dialog.get_values(); + if (!args) return; + var default_print_format = locals.DocType[me.doctype].default_print_format; + with_letterhead = args.with_letterhead ? 1 : 0; + print_format = args.print_sel ? args.print_sel : default_print_format; + + var json_string = JSON.stringify(valid_docs); + var w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + + 'doctype=' + encodeURIComponent(me.doctype) + + '&name=' + encodeURIComponent(json_string) + + '&format=' + encodeURIComponent(print_format) + + '&no_letterhead=' + (with_letterhead ? '0' : '1')); + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); return; + } + }); + + print_formats = frappe.meta.get_print_formats(me.doctype); + dialog.fields_dict.print_sel.$input.empty().add_options(print_formats); + + dialog.show(); + } + else { + frappe.msgprint(__('Select atleast 1 record for printing')) + } + }, true); + }, + + setup_like: function () { + var me = this; + this.$page.find('.result-list').on('click', '.like-action', frappe.ui.click_toggle_like); + this.list_header.find('.list-liked-by-me').on('click', function () { + if (me.is_star_filtered()) { + me.remove_liked_by_me(); + } else { + me.liked_by_me(); + } + }); + + if (!frappe.dom.is_touchscreen()) { + frappe.ui.setup_like_popover(this.$page.find('.result-list'), '.liked-by'); + } + }, + + setup_select_all: function () { + var me = this; + + if (this.can_delete || this.list_renderer.settings.selectable) { + this.list_header.find('.list-select-all').on('click', function () { + me.$page.find('.list-row-checkbox').prop('checked', $(this).prop('checked')); + }); + + this.$page.on('click', '.list-row-checkbox', function (event) { + // multi-select using shift key + var $this = $(this); + if (event.shiftKey && $this.prop('checked')) { + var $end_row = $this.parents('.list-row'); + var $start_row = $end_row.prevAll('.list-row') + .find('.list-row-checkbox:checked').last().parents('.list-row'); + if ($start_row) { + $start_row.nextUntil($end_row).find('.list-row-checkbox').prop('checked', true); + } + } + }); + } + }, + + setup_delete: function () { + var me = this; + if (!(this.can_delete || this.list_renderer.settings.selectable)) { + return; + } + this.$page.find('.list-row-checkbox').change(function () { + me.toggle_delete(); + }); + // after delete, hide delete button + this.wrapper.on('render-complete', function () { + me.toggle_delete(); + }); + }, + + toggle_delete: function () { + var checked_items = this.get_checked_items(); + var checked_items_status = this.$page.find('.checked-items-status'); + + if (checked_items.length > 0) { + this.page.set_primary_action(__('Delete'), function () { + me.delete_items() + }, 'octicon octicon-trashcan') + .addClass('btn-danger'); + + checked_items_status.text( + no_of_checked_items == 1 + ? __('1 item selected') + : __('{0} items selected', [checked_items.length]) + ) + checked_items_status.removeClass('hide'); + } else { + this.page.btn_primary.removeClass('btn-danger'); + this.set_primary_action(); + checked_items_status.addClass('hide'); + } + }, + + toggle_tags: function () { + if (this.tags_shown) { + $('.tag-row').addClass('hide'); + this.tags_shown = false; + } else { + $('.tag-row').removeClass('hide'); + this.tags_shown = true; + } + }, + + get_checked_items: function () { + var names = this.$page.find('.list-row-checkbox:checked').map(function (i, item) { + return $(item).data().name; + }).toArray(); + + return this.data.filter(function (doc) { + return names.includes(doc.name); + }); + }, + + set_primary_action: function () { + if (this.list_renderer.settings.set_primary_action) { + this.list_renderer.settings.set_primary_action(this); + } else { + this._super(); + } + }, + + delete_items: function () { + var me = this; + var to_delete = this.get_checked_items(); + if (!to_delete.length) + return; + + var docnames = to_delete.map(function (doc) { + return doc.name; + }); + + frappe.confirm(__('Delete {0} items permanently?', [to_delete.length]), + function () { + return frappe.call({ + method: 'frappe.desk.reportview.delete_items', + freeze: true, + args: { + items: docnames, + doctype: me.doctype + }, + callback: function () { + me.$page.find('.list-select-all').prop('checked', false); + frappe.utils.play_sound('delete'); + me.refresh(true); + } + }) + } + ); + }, + refresh_sidebar: function () { + //TODO: refresh if already exist + this.list_sidebar = new frappe.views.ListSidebar({ + doctype: this.doctype, + stats: this.list_renderer.stats, + parent: this.$page.find('.layout-side-section'), + set_filter: this.set_filter.bind(this), + default_filters: this.list_renderer.filters, + page: this.page, + list_view: this + }) + } +}); diff --git a/frappe/public/js/frappe/list/listview.js b/frappe/public/js/frappe/list/listview.js deleted file mode 100644 index bc44f258fe..0000000000 --- a/frappe/public/js/frappe/list/listview.js +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.views.get_listview = function(doctype, parent) { - if(frappe.listviews[doctype]) { - var listview = new frappe.listviews[doctype](parent); - } else { - var listview = new frappe.views.ListView(parent, doctype); - } - return listview; -} - -// Renders customized list -// usually based on `in_list_view` property - -frappe.views.ListView = Class.extend({ - init: function(doclistview, doctype) { - this.doclistview = doclistview; - this.doctype = doctype; - this.meta = frappe.get_doc("DocType", this.doctype); - this.image_field = this.meta.image_field || 'image'; - this.settings = frappe.listview_settings[this.doctype] || {}; - if(this.meta.__listview_template) { - this.template_name = doctype + "_listview"; - frappe.templates[this.template_name] = this.meta.__listview_template; - } - this.set_fields(); - this.set_columns(); - this.id_list = []; - if(this.settings.group_by) - this.group_by = this.settings.group_by; - - var me = this; - this.doclistview.onreset = function() { - me.id_list = []; - } - this.order_by = this.settings.order_by; - this.group_by = this.settings.group_by; - }, - set_fields: function() { - var me = this; - var t = "`tab"+this.doctype+"`."; - this.fields = []; - this.stats = ['_user_tags']; - - var add_field = function(fieldname) { - field = t + "`" + fieldname + "`" - if(me.fields.indexOf(field)=== -1) - me.fields.push(field); - } - - $.each(['name', 'owner', 'docstatus', '_user_tags', '_comments', 'modified', - 'modified_by', '_assign', '_liked_by', '_seen'], - function(i, fieldname) { add_field(fieldname); }) - - // add title field - if(this.meta.title_field) { - this.title_field = this.meta.title_field; - add_field(this.meta.title_field); - } - - // endabled / disabled - if(frappe.meta.has_field(this.doctype, 'enabled')) { add_field('enabled'); }; - if(frappe.meta.has_field(this.doctype, 'disabled')) { add_field('disabled'); }; - - // add workflow field (as priority) - this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype); - if(this.workflow_state_fieldname) { - if (!frappe.workflow.workflows[this.doctype]["override_status"]) { - add_field(this.workflow_state_fieldname); - } - this.stats.push(this.workflow_state_fieldname); - } - - $.each(this.meta.fields, function(i,d) { - if(d.in_list_view && frappe.perm.has_perm(me.doctype, d.permlevel, "read")) { - if(d.fieldtype=="Image" && d.options) { - add_field(d.options); - } else { - add_field(d.fieldname); - } - // currency field for symbol (multi-currency) - if(d.fieldtype=="Currency" && d.options) { - if(d.options.indexOf(":")!=-1) { - add_field(d.options.split(":")[1]); - } else { - add_field(d.options); - }; - } - } - }); - - // additional fields - if(this.settings.add_fields) { - $.each(this.settings.add_fields, function(i, d) { - if(d.indexOf("`tab")===-1) { - d = "`tab" + me.doctype + "`." + d; - } - if(me.fields.indexOf(d)==-1) - me.fields.push(d); - }); - } - - if(me.meta.__kanban_column_fields) - me.fields = me.fields.concat(me.meta.__kanban_column_fields); - }, - set_columns: function() { - var me = this; - this.columns = []; - var name_column = { - colspan: this.settings.colwidths && this.settings.colwidths.subject || 6, - type: "Subject", - title: "Name" - }; - if (this.meta.title_field) { - name_column.title = frappe.meta.get_docfield(this.doctype, this.meta.title_field).label; - } - this.columns.push(name_column); - this.total_colspans = this.columns[0].colspan; - - - if(frappe.has_indicator(this.doctype)) { - // indicator - this.columns.push({ - colspan: this.settings.colwidths && this.settings.colwidths.indicator || 3, - type: "Indicator", - title: "Status" - }); - this.total_colspans += this.columns[1].colspan; - } - - // overridden - var overridden = $.map(this.settings.add_columns || [], function(d) { - return d.content; - }); - var docfields_in_list_view = frappe.get_children("DocType", this.doctype, "fields", - {"in_list_view":1}).sort(function(a, b) { return a.idx - b.idx }) - - $.each(docfields_in_list_view, function(i,d) { - if(in_list(overridden, d.fieldname) || d.fieldname === me.title_field) { - return; - } - if(me.total_colspans < 12) { - me.add_column(d); - } - }); - - // additional columns - if(this.settings.add_columns) { - $.each(this.settings.add_columns, function(i, d) { - if(me.total_colspans < 12) { - if(typeof d==="string") { - me.add_column(frappe.meta.get_docfield(me.doctype, d)); - } else { - me.columns.push(d); - me.total_colspans += parseInt(d.colspan); - } - } - }); - } - - var empty_cols = flt(12 - this.total_colspans); - while(empty_cols > 0) { - for(var i=0, l=this.columns.length; i < l && empty_cols > 0; i++) { - this.columns[i].colspan = cint(this.columns[i].colspan) + 1; - empty_cols = empty_cols - 1; - } - } - }, - add_column: function(df) { - // field width - var colspan = 3; - if(in_list(["Int", "Percent"], df.fieldtype)) { - colspan = 2; - } else if(in_list(["Check", "Image"], df.fieldtype)) { - colspan = 1; - } else if(in_list(["name", "subject", "title"], df.fieldname)) { // subjects are longer - colspan = 4; - } else if(df.fieldtype=="Text Editor" || df.fieldtype=="Text") { - colspan = 4; - } - if(df.columns && df.columns>0){ - colspan = df.columns; - } - else if(this.settings.column_colspan && this.settings.column_colspan[df.fieldname]) { - colspan = this.settings.column_colspan[df.fieldname]; - } - this.total_colspans += parseInt(colspan); - var col = { - colspan: colspan, - content: df.fieldname, - type: df.fieldtype, - df:df, - fieldtype: df.fieldtype, - fieldname: df.fieldname, - title:__(df.label) - }; - if(this.settings.column_render && this.settings.column_render[df.fieldname]) { - col.render = this.settings.column_render[df.fieldname]; - } - this.columns.push(col); - - }, - render: function(row, data) { - this.prepare_data(data); - - // maintain id_list to avoid duplication incase - // of filtering by child table - if(in_list(this.id_list, data.name)) { - $(row).toggle(false); - return; - } else { - this.id_list.push(data.name); - } - - this['render_row_' + this.doclistview.current_view](row, data); - - if(this.settings.post_render_item) { - this.settings.post_render_item(this, row, data); - } - - this.render_tags(row, data); - - }, - render_row_List: function(row, data) { - var main = frappe.render_template("list_item_main", { - data: data, - columns: this.columns, - subject: this.get_avatar_and_id(data, true), - list: this, - right_column: this.settings.right_column - }); - - $(frappe.render_template("list_item_row", { - data: data, - main: main, - list: this, - right_column: this.settings.right_column - })).appendTo(row); - }, - render_row_Image: function(row, data) { - this.allowed_type = [ - "Check", "Currency", "Data", "Date", - "Datetime", "Float", "Int", "Link", - "Percent", "Select", "Read Only", "Time" - ]; - var image_url = (data.image && window.cordova && data.image.indexOf('http')===-1) ? - frappe.base_url + data[this.image_field] : data[this.image_field]; - - img_col = $(frappe.render_template("image_view_item_row", { - data: data, - list: this, - columns: this.columns, - allowed_type: this.allowed_type, - item_image: image_url ? "url('" + image_url + "')" : 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 = $('
    \ -
    \ -
    \ -
    ').appendTo(row); - - if(!me.doclistview.tags_shown) { - row2.addClass("hide"); - } - - // add tags - var tag_editor = new frappe.ui.TagEditor({ - parent: row2.find(".list-tag"), - frm: { - doctype: this.doctype, - docname: data.name - }, - list_sidebar: me.doclistview.list_sidebar, - user_tags: data._user_tags, - on_change: function(user_tags) { - data._user_tags = user_tags; - //me.render_timestamp_and_comments(row, data); - } - }); - tag_editor.wrapper.on("click", ".tagit-label", function() { - me.doclistview.set_filter("_user_tags", - $(this).text()); - }); - }, - - get_avatar_and_id: function(data, without_workflow) { - data._without_workflow = without_workflow; - data.css_seen = ''; - - if(data._seen) { - var seen = JSON.parse(data._seen); - if(seen && seen.indexOf(frappe.session.user) !== -1) { - data.css_seen = 'seen' - } - } - - return frappe.render_template("list_item_subject", data); - }, - - get_indicator: function(doc) { - var indicator = frappe.get_indicator(doc, this.doctype); - if(indicator) { - return ''+__(indicator[0])+''; - } else { - return ""; - } - }, - - get_indicator_dot: function(doc) { - var indicator = frappe.get_indicator(doc, this.doctype); - if (!indicator) { - return ""; - } - return ''; - }, - - prepare_data: function(data) { - if(data.modified) - this.prepare_when(data, data.modified); - - data._liked_by = data._liked_by ? - JSON.parse(data._liked_by) : []; - - data._checkbox = (frappe.model.can_delete(this.doctype) || this.settings.selectable) && !this.no_delete - - data._doctype_encoded = encodeURIComponent(data.doctype); - data._name = data.name.replace(/"/g, '\"'); - data._name_encoded = encodeURIComponent(data.name); - data._submittable = frappe.model.is_submittable(this.doctype); - - data._title = strip_html(data[this.title_field || "name"] || data["name"]); - data._full_title = data._title; - - if(data._title.length > 40) { - data._title = data._title.slice(0, 40) + "..."; - } - - data._workflow = null; - if(this.workflow_state_fieldname) { - data._workflow = { - fieldname: this.workflow_state_fieldname, - value: data[this.workflow_state_fieldname], - style: frappe.utils.guess_style(data[this.workflow_state_fieldname]) - } - } - data._user = user; - - data._tags = $.map((data._user_tags || "").split(","), - function(v) { return v ? v : null; }); - data._assign_list = data._assign ? JSON.parse(data._assign) : []; - - // nulls as strings - for(key in data) { - if(data[key]==null) { - data[key]=''; - } - } - - // prepare data in settings - if(this.settings.prepare_data) - this.settings.prepare_data(data); - }, - - prepare_when: function(data, date_str) { - if (!date_str) date_str = data.modified; - // when - data.when = (dateutil.str_to_user(date_str)).split(' ')[0]; - var diff = dateutil.get_diff(dateutil.get_today(), date_str.split(' ')[0]); - if(diff==0) { - data.when = comment_when(date_str); - } - if(diff == 1) { - data.when = __('Yesterday') - } - if(diff == 2) { - data.when = __('2 days ago') - } - }, - - render_bar_graph: function(parent, data, field, label) { - var args = { - percent: data[field], - label: __(label) - } - $(parent).append(repl(' \ - \ - ', args)); - }, - render_icon: function(parent, icon_class, label) { - var icon_html = ""; - $(parent).append(repl(icon_html, {icon_class: icon_class, label: __(label) || ''})); - } -}); diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js index a785a6eb99..674091eea9 100644 --- a/frappe/public/js/frappe/misc/utils.js +++ b/frappe/public/js/frappe/misc/utils.js @@ -185,20 +185,21 @@ frappe.utils = { me.intro_area = null; } }, - set_footnote: function(me, wrapper, txt) { - if(!me.footnote_area) { - me.footnote_area = $('
    ') + set_footnote: function(footnote_area, wrapper, txt) { + if(!footnote_area) { + footnote_area = $('
    ') .appendTo(wrapper); } if(txt) { - if(txt.search(/

    /)==-1) txt = '

    ' + txt + '

    '; - me.footnote_area.html(txt); + if(!txt.includes('

    ')) + txt = '

    ' + txt + '

    '; + footnote_area.html(txt); } else { - me.footnote_area.remove(); - me.footnote_area = null; + footnote_area.remove(); + footnote_area = null; } - return me.footnote_area; + return footnote_area; }, get_args_dict_from_url: function(txt) { var args = {}; @@ -593,3 +594,56 @@ frappe.utils = { return email_list; } }; + +// String.prototype.includes polyfill +// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/includes +if (!String.prototype.includes) { + String.prototype.includes = function(search, start) { + 'use strict'; + if (typeof start !== 'number') { + start = 0; + } + if (start + search.length > this.length) { + return false; + } else { + return this.indexOf(search, start) !== -1; + } + }; +} +// Array.prototype.includes polyfill +// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/includes +if (!Array.prototype.includes) { + Object.defineProperty(Array.prototype, 'includes', { + value: function(searchElement, fromIndex) { + 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 false; + } + var n = fromIndex | 0; + var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + while (k < len) { + if (o[k] === searchElement) { + return true; + } + k++; + } + return false; + } + }); +} +// Array de duplicate +if (!Array.prototype.uniqBy) { + Object.defineProperty(Array.prototype, 'uniqBy', { + value: function (key) { + var seen = {}; + return this.filter(function (item) { + var k = key(item); + return seen.hasOwnProperty(k) ? false : (seen[k] = true); + }) + } + }) +} \ No newline at end of file diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index eabb69a24e..6cfc82f51d 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -33,7 +33,7 @@ $.extend(frappe.model, { new_names: {}, events: {}, - list_settings: {}, + user_settings: {}, init: function() { // setup refresh if the document is updated somewhere else @@ -105,7 +105,6 @@ $.extend(frappe.model, { if(r.exc) { msgprint(__("Unable to load: {0}", [__(doctype)])); throw "No doctype"; - return; } if(r.message=="use_cache") { frappe.model.sync(cached_doc); @@ -115,10 +114,10 @@ $.extend(frappe.model, { frappe.model.init_doctype(doctype); frappe.defaults.set_user_permissions(r.user_permissions); - if(r.list_settings) { + if(r.user_settings) { // remember filters and other settings from last view - frappe.model.list_settings[doctype] = JSON.parse(r.list_settings); - frappe.model.list_settings[doctype].updated_on = moment().toString(); + frappe.model.user_settings[doctype] = JSON.parse(r.user_settings); + frappe.model.user_settings[doctype].updated_on = moment().toString(); } callback && callback(r); } diff --git a/frappe/public/js/frappe/model/user_settings.js b/frappe/public/js/frappe/model/user_settings.js new file mode 100644 index 0000000000..66b50375c3 --- /dev/null +++ b/frappe/public/js/frappe/model/user_settings.js @@ -0,0 +1,41 @@ +frappe.provide('frappe.model.user_settings'); + +$.extend(frappe.model.user_settings, { + save: function(doctype, key, value) { + var user_settings = frappe.model.user_settings[doctype] || {}; + + if ($.isPlainObject(value)) { + $.extend(user_settings[key], value); + } else { + user_settings[key] = value; + } + + return this.update(doctype, user_settings); + }, + remove: function(doctype, key) { + var user_settings = frappe.model.user_settings[doctype] || {}; + delete user_settings[key]; + + return this.update(doctype, user_settings); + }, + update: function(doctype, user_settings) { + return frappe.call({ + method: 'frappe.model.utils.user_settings.save', + args: { + doctype: doctype, + user_settings: user_settings + }, + callback: function(r) { + frappe.model.user_settings[doctype] = r.message; + } + }) + } +}); + +frappe.get_user_settings = function(doctype, key) { + var settings = frappe.model.user_settings[doctype] || {}; + if(key) { + settings = settings[key] || {}; + } + return settings; +} \ No newline at end of file diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 478a145c86..d04ce2b943 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -124,7 +124,8 @@ frappe.set_route = function() { frappe.route_options = a; return null; } else { - return a ? encodeURIComponent(a) : null; + return a; + // return a ? encodeURIComponent(a) : null; } }).join('/'); diff --git a/frappe/public/js/frappe/ui/base_list.js b/frappe/public/js/frappe/ui/base_list.js new file mode 100644 index 0000000000..a279e15199 --- /dev/null +++ b/frappe/public/js/frappe/ui/base_list.js @@ -0,0 +1,434 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +// new re-re-factored Listing object +// now called BaseList +// +// opts: +// parent + +// method (method to call on server) +// args (additional args to method) +// get_args (method to return args as dict) + +// show_filters [false] +// doctype +// filter_fields (if given, this list is rendered, else built from doctype) + +// query or get_query (will be deprecated) +// query_max +// buttons_in_frame + +// no_result_message ("No result") + +// page_length (20) +// hide_refresh (False) +// no_toolbar +// new_doctype +// [function] render_row(parent, data) +// [function] onrun +// no_loading (no ajax indicator) + +frappe.provide('frappe.ui'); + +frappe.ui.BaseList = Class.extend({ + init: function (opts) { + this.opts = opts || {}; + this.set_defaults(); + if (opts) { + this.make(); + } + }, + set_defaults: function () { + this.page_length = 20; + this.start = 0; + this.data = []; + }, + make: function (opts) { + if (opts) { + this.opts = opts; + } + this.prepare_opts(); + + $.extend(this, this.opts); + + // make dom + this.wrapper = $(frappe.render_template('listing', this.opts)); + this.parent.append(this.wrapper); + + this.set_events(); + + if (this.page) { + this.wrapper.find('.list-toolbar-wrapper').hide(); + } + + if (this.show_filters) { + this.make_filters(); + } + }, + prepare_opts: function () { + if (this.opts.new_doctype) { + if (!frappe.boot.user.can_create.includes(this.opts.new_doctype)) { + this.opts.new_doctype = null; + } + } + if (!this.opts.no_result_message) { + this.opts.no_result_message = __('Nothing to show'); + } + if (!this.opts.page_length) { + this.opts.page_length = this.user_settings && this.user_settings.limit || 20; + } + this.opts._more = __('More'); + }, + add_button: function (label, click, icon) { + if (this.page) { + return this.page.add_menu_item(label, click, icon) + } else { + this.wrapper.find('.list-toolbar-wrapper').removeClass('hide'); + return $('') + .appendTo(this.wrapper.find('.list-toolbar')) + .html((icon ? (' ') : '') + label) + .click(click); + } + }, + set_events: function () { + var me = this; + + // next page + this.wrapper.find('.btn-more').click(function () { + me.run(true); + }); + + this.wrapper.find(".btn-group-paging").on('click', '.btn', function () { + me.page_length = cint($(this).attr("data-value")); + + me.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info"); + $(this).addClass("btn-info"); + + // always reset when changing list page length + me.run(); + }); + + // select the correct page length + if (this.opts.page_length !== 20) { + this.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info"); + this.wrapper + .find(".btn-group-paging .btn[data-value='" + this.opts.page_length + "']") + .addClass('btn-info'); + } + + // title + if (this.title) { + this.wrapper.find('h3').html(this.title).show(); + } + + // new + this.set_primary_action(); + + if (me.no_toolbar || me.hide_toolbar) { + me.wrapper.find('.list-toolbar-wrapper').hide(); + } + }, + + set_primary_action: function () { + var me = this; + if (this.new_doctype) { + this.page.set_primary_action( + __("New"), + me.make_new_doc.bind(me, me.new_doctype), + "octicon octicon-plus" + ); + } else { + this.page.clear_primary_action(); + } + }, + + make_new_doc: function (doctype) { + var me = this; + frappe.model.with_doctype(doctype, function () { + if (me.custom_new_doc) { + me.custom_new_doc(doctype); + } else { + if (me.filter_list) { + frappe.route_options = {}; + me.filter_list.get_filters().forEach(function (f, i) { + if (f[2] === "=" && !frappe.model.std_fields_list.includes(f[1])) { + frappe.route_options[f[1]] = f[3]; + } + }); + } + frappe.new_doc(doctype, true); + } + }); + }, + + make_filters: function () { + this.filter_list = new frappe.ui.FilterList({ + base_list: this, + parent: this.wrapper.find('.list-filters').show(), + doctype: this.doctype, + filter_fields: this.filter_fields, + default_filters: this.default_filters || [] + }); + // default filter for submittable doctype + if (frappe.model.is_submittable(this.doctype)) { + this.filter_list.add_filter(this.doctype, "docstatus", "!=", 2); + }; + }, + + clear: function () { + this.data = []; + this.wrapper.find('.result-list').empty(); + this.wrapper.find('.result').show(); + this.wrapper.find('.no-result').hide(); + this.start = 0; + this.onreset && this.onreset(); + }, + + set_filters_from_route_options: function () { + var me = this; + this.filter_list.clear_filters(); + + for(var field in frappe.route_options) { + var value = frappe.route_options[field]; + var doctype = null; + + // if `Child DocType.fieldname` + if (field.includes(".")) { + doctype = field.split(".")[0]; + field = field.split(".")[1]; + } + + // find the table in which the key exists + // for example the filter could be {"item_code": "X"} + // where item_code is in the child table. + + // we can search all tables for mapping the doctype + if (!doctype) { + doctype = frappe.meta.get_doctype_for_field(me.doctype, field); + } + + if (doctype) { + if ($.isArray(value)) { + me.filter_list.add_filter(doctype, field, value[0], value[1]); + } else { + me.filter_list.add_filter(doctype, field, "=", value); + } + } + } + frappe.route_options = null; + }, + + run: function (more) { + var me = this; + if (!more) { + this.start = 0; + this.onreset && this.onreset(); + } + + var args = this.get_call_args(); + this.save_user_settings_locally(args); + + // user_settings are saved by db_query.py when dirty + $.extend(args, { + user_settings: frappe.model.user_settings[this.doctype] + }); + + return frappe.call({ + method: this.opts.method || 'frappe.desk.query_builder.runquery', + type: "GET", + freeze: this.opts.freeze !== undefined ? this.opts.freeze : true, + args: args, + callback: function (r) { + me.dirty = false; + me.render_results(r); + }, + no_spinner: this.opts.no_loading + }); + }, + save_user_settings_locally: function (args) { + if (this.opts.save_user_settings && this.doctype && !this.docname) { + // save list settings locally + var user_settings = frappe.model.user_settings[this.doctype]; + var different = false; + + if (!user_settings) { + return; + } + + if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) { + // settings are dirty if filters change + user_settings.filters = args.filters; + different = true; + } + + if (user_settings.order_by !== args.order_by) { + user_settings.order_by = args.order_by; + different = true; + } + + if (user_settings.limit !== args.limit_page_length) { + user_settings.limit = args.limit_page_length || 20 + different = true; + } + + // save fields in list settings + if (args.save_user_settings_fields) { + user_settings.fields = args.fields; + } + + if (different) { + user_settings.updated_on = moment().toString(); + } + } + }, + get_call_args: function () { + // load query + if (!this.method) { + var query = this.get_query && this.get_query() || this.query; + query = this.add_limits(query); + var args = { + query_max: this.query_max, + as_dict: 1 + } + args.simple_query = query; + } else { + var args = { + start: this.start, + page_length: this.page_length + } + } + + // append user-defined arguments + if (this.args) + $.extend(args, this.args) + + if (this.get_args) { + $.extend(args, this.get_args()); + } + return args; + }, + render_results: function (r) { + if (this.start === 0) + this.clear(); + + this.wrapper.find('.btn-more, .list-loading').hide(); + + var values = []; + + if (r.message) { + values = this.get_values_from_response(r.message); + } + + if (values.length || !this.show_no_result()) { + this.data = this.data.concat(values); + this.render_view(values); + this.update_paging(values); + } else if (this.start === 0) { + // show no result message + this.wrapper.find('.result').hide(); + + var msg = ''; + var no_result_message = this.no_result_message; + if(no_result_message && $.isFunction(no_result_message)) { + msg = no_result_message(); + } else if(typeof no_result_message === 'string') { + msg = no_result_message; + } else { + msg = __('No Results') + } + + this.wrapper.find('.no-result').html(msg).show(); + } + + this.wrapper.find('.list-paging-area') + .toggle(values.length || this.start > 0); + + // callbacks + if (this.onrun) this.onrun(); + if (this.callback) this.callback(r); + this.wrapper.trigger("render-complete"); + }, + + get_values_from_response: function (data) { + // make dictionaries from keys and values + if (data.keys && $.isArray(data.keys)) { + return frappe.utils.dict(data.keys, data.values); + } else { + return data; + } + }, + + render_view: function (values) { + // override this method in derived class + }, + + update_paging: function (values) { + if (values.length >= this.page_length) { + this.wrapper.find('.btn-more').show(); + this.start += this.page_length; + } + }, + + refresh: function () { + this.run(); + }, + add_limits: function (query) { + return query + ' LIMIT ' + this.start + ',' + (this.page_length + 1); + }, + set_filter: function (fieldname, label, no_run, no_duplicate) { + var filter = this.filter_list.get_filter(fieldname); + if (filter) { + var value = cstr(filter.field.get_parsed_value()); + if (value.includes(label)) { + // already set + return false + + } else if (no_duplicate) { + filter.set_values(this.doctype, fieldname, "=", label); + } else { + // second filter set for this field + if (fieldname == '_user_tags' || fieldname == "_liked_by") { + // and for tags + this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%'); + } else { + // or for rest using "in" + filter.set_values(this.doctype, fieldname, 'in', value + ', ' + label); + } + } + } else { + // no filter for this item, + // setup one + if (['_user_tags', '_comments', '_assign', '_liked_by'].includes(fieldname)) { + this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%'); + } else { + this.filter_list.add_filter(this.doctype, fieldname, '=', label); + } + } + if (!no_run) + this.run(); + }, + init_user_settings: function () { + this.user_settings = frappe.model.user_settings[this.doctype] || {}; + }, + call_for_selected_items: function (method, args) { + var me = this; + args.names = this.get_checked_items().map(function (item) { + return item.name; + }); + + frappe.call({ + method: method, + args: args, + freeze: true, + callback: function (r) { + if (!r.exc) { + if (me.list_header) { + me.list_header.find(".list-select-all").prop("checked", false); + } + me.refresh(); + } + } + }); + } +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js index 36d0736cd2..7b152aa4d1 100644 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ b/frappe/public/js/frappe/ui/filters/filters.js @@ -5,7 +5,7 @@ frappe.ui.FilterList = Class.extend({ init: function(opts) { $.extend(this, opts); this.filters = []; - this.wrapper = this.$parent; + this.wrapper = this.parent; this.stats = []; this.make(); this.set_events(); @@ -13,6 +13,20 @@ frappe.ui.FilterList = Class.extend({ make: function() { var me = this; + this.wrapper.find('.show_filters').remove(); + this.wrapper.append(` +
    +
    + + +
    +
    `); $(frappe.render_template("filter_dashboard", {})).appendTo(this.wrapper.find('.show_filters')); //show filter dashboard @@ -85,7 +99,7 @@ frappe.ui.FilterList = Class.extend({ args: { stats: me.stats, doctype: me.doctype, - filters:me.default_filters + filters: me.default_filters }, callback: function(r) { // This gives a predictable stats order @@ -163,9 +177,9 @@ frappe.ui.FilterList = Class.extend({ var noduplicate = true } if (label=="No Data"){ - me.listobj.set_filter(fieldname, '', false, noduplicate); + me.base_list.set_filter(fieldname, '', false, noduplicate); }else{ - me.listobj.set_filter(fieldname, label, false, noduplicate); + me.base_list.set_filter(fieldname, label, false, noduplicate); } return false; }) @@ -205,9 +219,9 @@ frappe.ui.FilterList = Class.extend({ var noduplicate = true } if (item.label == "No Data") { - me.listobj.set_filter(item.value, '', false, noduplicate); + me.base_list.set_filter(item.value, '', false, noduplicate); } else { - me.listobj.set_filter(item.value, item.label, false, noduplicate); + me.base_list.set_filter(item.value, item.label, false, noduplicate); } } }); @@ -222,8 +236,8 @@ frappe.ui.FilterList = Class.extend({ this.wrapper.find('.clear-filters').bind('click', function() { me.clear_filters(); - $('.date-range-picker').val(''); - me.listobj.run(); + $('.date-range-picker').val('') + me.base_list.run(); $(this).addClass("hide"); }); @@ -247,8 +261,7 @@ frappe.ui.FilterList = Class.extend({ df: { fieldtype: "Check", fieldname: "is_date_range", - label: __("Date Range"), - input_css: { "margin-top": "-2px" } + label: __("Date Range") } }); check.change = function() { @@ -283,11 +296,11 @@ frappe.ui.FilterList = Class.extend({ filt && filt.remove(true); if(!dateObj.length && dateObj && date.datepicker.opts.range===false) { me.add_filter(me.doctype, name, '=', moment(dateObj).format('YYYY-MM-DD')); - me.listobj.run(); + me.base_list.run(); } else if(dateObj.length===2 && date.datepicker.opts.range===true) { me.add_filter(me.doctype, name, 'Between', [moment(dateObj[0]).format('YYYY-MM-DD'), moment(dateObj[1]).format('YYYY-MM-DD')]); - me.listobj.run(); + me.base_list.run(); } }); } @@ -444,7 +457,7 @@ frappe.ui.Filter = Class.extend({ this.wrapper.find(".set-filter-and-run").on("click", function() { me.wrapper.removeClass("is-new-filter"); - me.flist.listobj.run(); + me.flist.base_list.run(); }); // add help for "in" codition @@ -482,9 +495,8 @@ frappe.ui.Filter = Class.extend({ this.flist.update_filters(); if(!dont_run) { - this.flist.listobj.dirty = true; - this.flist.listobj.clean_dash = true; - this.flist.listobj.refresh(); + this.flist.base_list.clean_dash = true; + this.flist.base_list.refresh(true); } }, @@ -559,7 +571,7 @@ frappe.ui.Filter = Class.extend({ // run on enter $(me.field.wrapper).find(':input').keydown(function(ev) { if(ev.which==13) { - me.flist.listobj.run(); + me.flist.base_list.run(); } }) }, diff --git a/frappe/public/js/frappe/ui/listing.html b/frappe/public/js/frappe/ui/listing.html index cd745b0056..9700abf918 100644 --- a/frappe/public/js/frappe/ui/listing.html +++ b/frappe/public/js/frappe/ui/listing.html @@ -1,18 +1,5 @@
    diff --git a/frappe/public/js/frappe/ui/listing.js b/frappe/public/js/frappe/ui/listing.js index 203374b27a..1876e4ccb1 100644 --- a/frappe/public/js/frappe/ui/listing.js +++ b/frappe/public/js/frappe/ui/listing.js @@ -224,6 +224,11 @@ frappe.ui.Listing = Class.extend({ var args = this.get_call_args(); this.save_list_settings_locally(args); + // list_settings are saved by db_query.py when dirty + $.extend(args, { + list_settings: frappe.model.list_settings[this.doctype] + }); + return frappe.call({ method: this.opts.method || 'frappe.desk.query_builder.runquery', type: "GET", @@ -251,7 +256,7 @@ frappe.ui.Listing = Class.extend({ if(!frappe.utils.arrays_equal(args.filters, list_settings.filters)) { //dont save filters in Kanban view - if(!frappe.get_route()[2]==="Kanban") { + if(this.current_view!=="Kanban") { // settings are dirty if filters change list_settings.filters = args.filters || []; different = true; @@ -271,7 +276,7 @@ frappe.ui.Listing = Class.extend({ // save fields in list settings if(args.save_list_settings_fields) { list_settings.fields = args.fields; - }; + } if(different) { list_settings.updated_on = moment().toString(); @@ -318,9 +323,10 @@ frappe.ui.Listing = Class.extend({ r.values = this.get_values_from_response(r.message); } - if(r.values.length) { + if(r.values.length || this.force_render_view) { this.data = this.data.concat(r.values); - this.render_list(r.values); + this.render_view(r.values); + // this.render_list(r.values); this.update_paging(r.values); } else { if(this.start===0) { @@ -355,11 +361,20 @@ frappe.ui.Listing = Class.extend({ } }, + render_view: function(values) { + this.list_view = new frappe.views.ListView({ + doctype: this.doctype, + values: values, + }); + }, + render_list: function(values) { - this.last_page = values; - if(this.filter_list) { - this.filter_values = this.filter_list.get_filters(); - } + // TODO: where is this used? + // this.last_page = values; + // if(this.filter_list) { + // // and this? + // this.filter_values = this.filter_list.get_filters(); + // } this.render_rows(values); }, @@ -370,33 +385,6 @@ frappe.ui.Listing = Class.extend({ this.render_row(this.add_row(values[i]), values[i], this, i); } }, - 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.wrapper.find(".blueimp-gallery").remove(); - // append gallery div - var gallery = frappe.render_template("blueimp-gallery", {}); - $(gallery).appendTo(me.wrapper); - - me.wrapper.find(".zoom-view").click(function(event){ - event.preventDefault(); - opts = { - doctype: me.doctype, - docname: $(this).parent().attr('data-name'), - container: me.wrapper - }; - new frappe.views.ImageView(opts); - }); - }); - }, update_paging: function(values) { if(values.length >= this.page_length) { this.wrapper.find('.btn-more').toggle(true); diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index f6e87f0014..83a6baa3c4 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -80,7 +80,6 @@ frappe.ui.Page = Class.extend({ this.page_actions = this.wrapper.find(".page-actions"); - this.checked_items_status = this.page_actions.find(".checked-items-status"); this.btn_primary = this.page_actions.find(".primary-action"); this.btn_secondary = this.page_actions.find(".btn-secondary"); diff --git a/frappe/public/js/frappe/ui/sort_selector.js b/frappe/public/js/frappe/ui/sort_selector.js index e83105af1f..0906771dc2 100644 --- a/frappe/public/js/frappe/ui/sort_selector.js +++ b/frappe/public/js/frappe/ui/sort_selector.js @@ -14,6 +14,7 @@ frappe.ui.SortSelector = Class.extend({ }, make: function() { this.prepare_args(); + this.parent.find('.sort-selector').remove(); this.wrapper = $(frappe.render_template('sort_selector', this.args)).appendTo(this.parent); this.bind_events(); }, @@ -46,6 +47,26 @@ frappe.ui.SortSelector = Class.extend({ if(!this.args) { this.args = {}; } + + // args as string + if(this.args && typeof this.args === 'string') { + var order_by = this.args; + this.args = {} + + if (order_by.includes('`.`')) { + // scrub table name (separated by dot), like `tabTime Log`.`modified` desc` + order_by = order_by.split('.')[1]; + } + + var parts = order_by.split(' '); + if (parts.length === 2) { + var fieldname = strip(parts[0], '`'); + + this.args.sort_by = fieldname; + this.args.sort_order = parts[1]; + } + } + if(this.args.options) { this.args.options.forEach(function(o) { me.labels[o.fieldname] = o.label; @@ -114,13 +135,8 @@ frappe.ui.SortSelector = Class.extend({ _options.push({'fieldname': 'idx'}); // de-duplicate - var added = []; - this.args.options = []; - _options.forEach(function(o) { - if(added.indexOf(o.fieldname)===-1) { - me.args.options.push(o); - added.push(o.fieldname); - } + this.args.options = _options.uniqBy(function(obj) { + return obj.fieldname; }); // add missing labels diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 9142095dd6..d795d74d71 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -350,7 +350,7 @@ frappe.search.AwesomeBar = Class.extend({ var route = frappe.get_route(); if(route[0]==="List" && txt.indexOf(" in") === -1) { // search in title field - var meta = frappe.get_meta(frappe.container.page.doclistview.doctype); + var meta = frappe.get_meta(frappe.container.page.list_view.doctype); var search_field = meta.title_field || "name"; var options = {}; options[search_field] = ["like", "%" + txt + "%"]; diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 99f3cec885..ec2ecd2300 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -109,7 +109,7 @@ frappe.views.show_open_count_list = function(element) { var route = frappe.get_route(); if(route[0]==="List" && route[1]===doctype) { - frappe.pages["List/" + doctype].doclistview.refresh(); + frappe.pages["List/" + doctype].list_view.refresh(); } else { frappe.set_route("List", doctype); } diff --git a/frappe/public/js/frappe/views/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js similarity index 89% rename from frappe/public/js/frappe/views/calendar.js rename to frappe/public/js/frappe/views/calendar/calendar.js index a7a3fa1681..cb0165e2b2 100644 --- a/frappe/public/js/frappe/views/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -4,6 +4,35 @@ frappe.provide("frappe.views.calendar"); frappe.provide("frappe.views.calendars"); +frappe.views.CalendarView = frappe.views.ListRenderer.extend({ + name: 'Calendar', + render_view: function() { + var me = this; + var options = { + doctype: this.doctype, + parent: this.wrapper, + page: this.list_view.page, + filter_vals: this.list_view.filter_list.get_filters() + } + $.extend(options, frappe.views.calendar[this.doctype]); + this.calendar = new frappe.views.Calendar(options); + }, + set_defaults: function() { + this._super(); + this.page_title = this.page_title + ' ' + __('Calendar'); + this.no_realtime = true; + this.show_no_result = false; + this.hide_sort_selector = true; + }, + get_header_html: function() { + return null; + }, + required_libs: [ + 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css', + 'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js' + ] +}) + frappe.views.Calendar = Class.extend({ init: function(options) { $.extend(this, options); @@ -14,20 +43,6 @@ frappe.views.Calendar = Class.extend({ make_page: function() { var me = this; - $(this.parent).on("show", function() { - me.set_filters_from_route_options(); - }); - - var module = locals.DocType[this.doctype].module; - this.page.set_title(__("Calendar") + " - " + __(this.doctype)); - - frappe.breadcrumbs.add(module, this.doctype); - - this.page.set_primary_action(__("New"), function() { - var doc = frappe.model.get_new_doc(me.doctype); - frappe.set_route("Form", me.doctype, doc.name); - }); - // add links to other calendars $.each(frappe.boot.calendars, function(i, doctype) { if(frappe.model.can_read(doctype)) { @@ -46,9 +61,9 @@ frappe.views.Calendar = Class.extend({ var me = this; this.$wrapper = this.parent; this.$cal = $("
    ").appendTo(this.$wrapper); - footnote = frappe.utils.set_footnote(this, this.$wrapper, + this.footnote_area = frappe.utils.set_footnote(this.footnote_area, this.$wrapper, __("Select or drag across time slots to create a new event.")); - footnote.css({"border-top": "0px"}); + this.footnote_area.css({"border-top": "0px"}); this.$cal.fullCalendar(this.cal_options); this.set_css(); diff --git a/frappe/public/js/frappe/views/container.js b/frappe/public/js/frappe/views/container.js index d21ebbee9c..81ed6e2eab 100644 --- a/frappe/public/js/frappe/views/container.js +++ b/frappe/public/js/frappe/views/container.js @@ -12,7 +12,7 @@ frappe.views.Container = Class.extend({ init: function() { this.container = $('#body_div').get(0); this.page = null; // current page - this.pagewidth = $('#body_div').width(); + this.pagewidth = $(this.container).width(); this.pagemargin = 50; var me = this; @@ -36,7 +36,7 @@ frappe.views.Container = Class.extend({ var page = $('
    ') .attr('id', "page-" + label) .attr("data-page-route", label) - .toggle(false) + .hide() .appendTo(this.container).get(0); page.label = label; frappe.pages[label] = page; @@ -69,7 +69,7 @@ frappe.views.Container = Class.extend({ // hide current if(this.page && this.page != page) { - $(this.page).toggle(false); + $(this.page).hide(); $(this.page).trigger('hide'); } @@ -77,7 +77,7 @@ frappe.views.Container = Class.extend({ if(!this.page || this.page != page) { this.page = page; // $(this.page).fadeIn(300); - $(this.page).toggle(true); + $(this.page).show(); } $(document).trigger("page-change"); diff --git a/frappe/public/js/frappe/views/factory.js b/frappe/public/js/frappe/views/factory.js index 79d629b277..f354f51767 100644 --- a/frappe/public/js/frappe/views/factory.js +++ b/frappe/public/js/frappe/views/factory.js @@ -11,7 +11,7 @@ frappe.views.Factory = Class.extend({ show: function() { var page_name = frappe.get_route_str(), me = this; - if(frappe.pages[page_name] && page_name.indexOf("Form/")===-1) { + if(frappe.pages[page_name] && !page_name.includes("Form/")) { frappe.container.change_to(frappe.pages[page_name]); if(me.on_show) { me.on_show(); diff --git a/frappe/public/js/frappe/views/gantt/gantt_view.js b/frappe/public/js/frappe/views/gantt/gantt_view.js new file mode 100644 index 0000000000..fb99ea292d --- /dev/null +++ b/frappe/public/js/frappe/views/gantt/gantt_view.js @@ -0,0 +1,201 @@ +frappe.provide('frappe.views'); + +frappe.views.GanttView = frappe.views.ListRenderer.extend({ + name: 'Gantt', + prepare: function(values) { + this.items = values; + this.prepare_tasks(); + this.prepare_dom(); + }, + + render_view: function(values) { + var me = this; + this.prepare(values); + this.render_gantt(); + }, + + set_defaults: function() { + this._super(); + this.no_realtime = true; + this.page_title = this.page_title + ' ' + __('Gantt'); + }, + + init_settings: function() { + this._super(); + this.field_map = frappe.views.calendar[this.doctype].field_map; + this.order_by = this.order_by || this.field_map.start + ' asc'; + }, + + prepare_dom: function() { + this.wrapper.css('overflow', 'auto') + .append('') + }, + + render_gantt: function(tasks) { + var me = this; + this.gantt_view_mode = this.user_settings.gantt_view_mode || 'Day'; + var field_map = frappe.views.calendar[this.doctype].field_map; + + this.gantt = new Gantt(".gantt-container", this.tasks, { + view_mode: this.gantt_view_mode, + date_format: "YYYY-MM-DD", + on_click: function (task) { + frappe.set_route('Form', task.doctype, task.id); + }, + on_date_change: function(task, start, end) { + if(!me.can_write()) return; + me.update_gantt_task(task, start, end); + }, + on_progress_change: function(task, progress) { + if(!me.can_write()) return; + var progress_fieldname = 'progress'; + + if($.isFunction(field_map.progress)) { + progress_fieldname = null; + } else if(field_map.progress) { + progress_fieldname = field_map.progress; + } + + if(progress_fieldname) { + frappe.db.set_value(task.doctype, task.id, + progress_fieldname, parseInt(progress)); + } + }, + on_view_change: function(mode) { + // save view mode + frappe.model.user_settings.save(me.doctype, 'Gantt', { + gantt_view_mode: mode + }); + }, + custom_popup_html: function(task) { + var item = me.get_item(task.id); + var list_item_subject = frappe.render_template('list_item_subject', item); + var html = '
    '+ + list_item_subject +'
    '; + + // custom html in {doctype}_list.js + var custom = me.settings.gantt_custom_popup_html; + if(custom) { + html = custom(item, html); + } + + return '
    '+ html +'
    '; + } + }); + this.render_dropdown(); + }, + + render_dropdown: function() { + var me = this; + var view_modes = this.gantt.config.view_modes || []; + var dropdown = ""; + + // view modes (for translation) __("Day"), __("Week"), __("Month"), + //__("Half Day"), __("Quarter Day") + + var dropdown_list = ""; + view_modes.forEach(function(view_mode) { + dropdown_list += "
  • " + + "" + + __(view_mode) + "
  • "; + }); + var $dropdown = $(dropdown) + $dropdown.find(".dropdown-menu") + .append(dropdown_list); + me.list_view.$page.find(`.list-row-head[data-list-renderer='Gantt'] .list-row-right`).css("margin-top", 0).html($dropdown) + $dropdown.on("click", ".option", function() { + var mode = $(this).data('value'); + me.gantt.change_view_mode(mode); + $dropdown.find(".dropdown-text").text(mode); + }); + }, + + prepare_tasks: function() { + var me = this; + var meta = frappe.get_meta(this.doctype); + var field_map = frappe.views.calendar[this.doctype].field_map; + this.tasks = this.items.map(function(item) { + // set progress + var progress = 0; + if(field_map.progress && $.isFunction(field_map.progress)) { + progress = field_map.progress(item); + } else if(field_map.progress) { + progress = item[field_map.progress] + } + + // title + if(meta.title_field) { + var label = $.format("{0} ({1})", [item[meta.title_field], item.name]); + } else { + var label = item[field_map.title]; + } + + return { + start: item[field_map.start], + end: item[field_map.end], + name: label, + id: item[field_map.id || 'name'], + doctype: me.doctype, + progress: progress, + dependencies: item.depends_on_tasks || "" + }; + }); + }, + get_item: function(name) { + return this.items.find(function(item) { + return item.name === name; + }); + }, + update_gantt_task: function(task, start, end) { + var me = this; + if(me.gantt.updating_task) { + setTimeout(me.update_gantt_task.bind(me, task, start, end), 200) + return; + } + me.gantt.updating_task = true; + + var field_map = frappe.views.calendar[this.doctype].field_map; + frappe.call({ + method: 'frappe.desk.gantt.update_task', + args: { + args: { + doctype: task.doctype, + name: task.id, + start: start.format('YYYY-MM-DD'), + end: end.format('YYYY-MM-DD') + }, + field_map: field_map + }, + callback: function() { + me.gantt.updating_task = false; + show_alert({message:__("Saved"), indicator: 'green'}, 1); + } + }); + }, + get_header_html: function() { + return frappe.render_template('list_item_row_head', { main: '', list: this }); + }, + refresh: function(values) { + this.prepare(values); + this.render(); + }, + can_write: function() { + if(frappe.model.can_write(this.doctype)) { + return true; + } else { + // reset gantt state + this.gantt.change_view_mode(this.gantt_view_mode); + show_alert({message: __("Not permitted"), indicator: 'red'}, 1); + return false; + } + }, + set_columns: function() {}, + required_libs: [ + "assets/frappe/js/lib/snap.svg-min.js", + "assets/frappe/js/lib/frappe-gantt/frappe-gantt.js" + ] +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/image/image_view.js b/frappe/public/js/frappe/views/image/image_view.js new file mode 100644 index 0000000000..4d9b4321a6 --- /dev/null +++ b/frappe/public/js/frappe/views/image/image_view.js @@ -0,0 +1,180 @@ +/** + * frappe.views.ImageView + */ +frappe.provide("frappe.views"); + +frappe.views.ImageView = frappe.views.ListRenderer.extend({ + name: 'Image', + render_view: function (values) { + this.items = values; + this.render_image_view(); + this.setup_gallery(); + }, + set_defaults: function() { + this._super(); + this.page_title = this.page_title + ' ' + __('Images'); + }, + render_image_view: function () { + var html = this.items.map(this.render_item.bind(this)).join(""); + this.container = $('
    ') + .addClass('image-view-container') + .appendTo(this.wrapper); + this.container.append(html); + }, + render_item: function (item) { + var image_url = this.get_image_url(item); + var indicator = this.get_indicator_html(item); + return frappe.render_template("image_view_item_row", { + data: item, + indicator: indicator, + additional_columns: this.additional_columns, + item_image: image_url, + color: frappe.get_palette(item.item_name) + }); + }, + get_image_url: function (item) { + var url; + url = item.image ? item.image : item[this.meta.image_field]; + + // absolute url for mobile + if (window.cordova && !frappe.utils.is_url(url)) { + url = frappe.base_url + url; + } + if (url) { + return url + // return "url('" + url + "')"; + } + return null; + }, + get_header_html: function () { + var main = frappe.render_template('image_view_item_main_head', { + columns: this.columns, + right_column: this.settings.right_column, + _checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable) + && !this.no_delete) + }); + return frappe.render_template('list_item_row_head', { main: main, list: this }); + }, + setup_gallery: function() { + var me = this; + var gallery = new frappe.views.GalleryView({ + doctype: this.doctype, + items: this.items, + wrapper: this.container + }); + this.container.on('click', '.btn.zoom-view', function(e) { + e.preventDefault(); + e.stopPropagation(); + var name = $(this).data().name; + gallery.show(name); + return false; + }); + }, + refresh: this.render_view +}); + +frappe.views.GalleryView = Class.extend({ + init: function(opts) { + $.extend(this, opts); + var me = this; + + this.ready = false; + this.load_lib(function() { + me.prepare(); + me.ready = true; + }); + }, + prepare: function() { + // keep only one pswp dom element + this.pswp_root = $('body > .pswp'); + if(this.pswp_root.length === 0) { + var pswp = frappe.render_template('photoswipe_dom'); + this.pswp_root = $(pswp).appendTo('body'); + } + }, + show: function(docname) { + var me = this; + if(!this.ready) { + setTimeout(this.show.bind(this), 200); + return; + } + var items = this.items.map(function(i) { + var query = 'img[data-name="'+i.name+'"]'; + var el = me.wrapper.find(query).get(0); + return { + src: i.image, + msrc: i.image, + name: i.name, + w: el.naturalWidth, + h: el.naturalHeight, + el: el + } + }); + + var index; + items.map(function(item, i) { + if(item.name === docname) + index = i; + }); + + var options = { + index: index, + getThumbBoundsFn: function(index) { + var thumbnail = items[index].el, // find thumbnail + pageYScroll = window.pageYOffset || document.documentElement.scrollTop, + rect = thumbnail.getBoundingClientRect(); + + return {x:rect.left, y:rect.top + pageYScroll, w:rect.width}; + }, + history: false, + shareEl: false, + } + var pswp = new PhotoSwipe( + this.pswp_root.get(0), + PhotoSwipeUI_Default, + items, + options + ); + pswp.init(); + }, + get_image_urls: function() { + // not implemented yet + return frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "File", + order_by: "attached_to_name", + fields: [ + "'image/*' as type", "ifnull(thumbnail_url, file_url) as thumbnail", + "concat(attached_to_name, ' - ', file_name) as title", "file_url as src", + "attached_to_name as name" + ], + filters: [ + ["File", "attached_to_doctype", "=", this.doctype], + ["File", "attached_to_name", "in", this.docnames], + ["File", "is_folder", "!=", 1] + ] + }, + freeze: true, + freeze_message: "Fetching Images.." + }).then(function(r) { + if (!r.message) { + msgprint("No Images found") + } else { + // filter image files from other + var images = r.message.filter(function(image) { + return frappe.utils.is_image_file(image.title || image.href); + }); + } + }); + }, + load_lib: function(callback) { + var asset_dir = 'assets/frappe/js/lib/photoswipe/'; + frappe.require([ + asset_dir + 'photoswipe.css', + asset_dir + 'default-skin.css', + asset_dir + 'photoswipe.js', + asset_dir + 'photoswipe-ui-default.js' + ], callback); + } +}); \ 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/views/image/image_view_item_main_head.html similarity index 77% rename from frappe/public/js/frappe/list/image_view_item_main_head.html rename to frappe/public/js/frappe/views/image/image_view_item_main_head.html index 1b2cf4aa7d..f0650af4dc 100644 --- a/frappe/public/js/frappe/list/image_view_item_main_head.html +++ b/frappe/public/js/frappe/views/image/image_view_item_main_head.html @@ -1,5 +1,7 @@
    +
    {%= frappe.render_template("header_select_all_like_filter", { _checkbox: _checkbox }) %} +
    \ No newline at end of file diff --git a/frappe/public/js/frappe/views/image/image_view_item_row.html b/frappe/public/js/frappe/views/image/image_view_item_row.html new file mode 100644 index 0000000000..cab60a6ccc --- /dev/null +++ b/frappe/public/js/frappe/views/image/image_view_item_row.html @@ -0,0 +1,44 @@ +
    +
    +
    + {{ frappe.render_template("list_item_subject", data) }} +
    +
    + + + +
    diff --git a/frappe/public/js/frappe/views/image/photoswipe_dom.html b/frappe/public/js/frappe/views/image/photoswipe_dom.html new file mode 100644 index 0000000000..ce4b06dce6 --- /dev/null +++ b/frappe/public/js/frappe/views/image/photoswipe_dom.html @@ -0,0 +1,69 @@ + + + + + \ No newline at end of file diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js new file mode 100644 index 0000000000..511a334920 --- /dev/null +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -0,0 +1,1048 @@ +frappe.provide("frappe.views"); + +(function () { + + var method_prefix = 'frappe.desk.doctype.kanban_board.kanban_board.'; + var saving_filters = false; + + var store = fluxify.createStore({ + id: 'store', + initialState: { + doctype: '', + board: {}, + card_meta: {}, + cards: [], + columns: [], + filters_modified: false, + cur_list: {} + }, + actionCallbacks: { + init: function (updater, opts) { + get_board(opts.board_name) + .then(function (board) { + var card_meta = get_card_meta(opts); + opts.card_meta = card_meta; + opts.board = board; + var cards = opts.cards.map(function (card) { + return prepare_card(card, opts); + }); + var columns = prepare_columns(board.columns); + + // save kanban board name in user_settings + frappe.model.user_settings.save(opts.doctype, 'Kanban', { + last_kanban_board: opts.board_name + }); + + updater.set({ + doctype: opts.doctype, + board: board, + card_meta: card_meta, + cards: cards, + columns: columns, + cur_list: opts.cur_list + }); + }); + }, + add_column: function (updater, col) { + fluxify.doAction('update_column', col, 'add'); + }, + archive_column: function (updater, col) { + fluxify.doAction('update_column', col, 'archive'); + }, + restore_column: function (updater, col) { + fluxify.doAction('update_column', col, 'restore'); + }, + update_column: function (updater, col, action) { + var doctype = this.doctype; + var board = this.board; + fetch_customization(doctype) + .then(function (doc) { + return modify_column_field_in_c11n(doc, board, col.title, action) + }) + .then(save_customization) + .then(function (r) { + return update_kanban_board(board.name, col.title, action) + }).then(function (r) { + var cols = r.message; + updater.set({ + columns: prepare_columns(cols) + }); + }, function (err) { + console.error(err); + }); + }, + set_filter_state: function (updater) { + is_filters_modified(this.board, this.cur_list) + .then(function(flag) { + updater.set({ + filters_modified: flag + }); + }); + }, + save_filters: function (updater) { + if(saving_filters) return; + saving_filters = true; + var filters = JSON.stringify(this.cur_list.filter_list.get_filters()); + frappe.call({ + method: method_prefix + 'save_filters', + args: { + board_name: this.board.name, + filters: filters + } + }).then(function(r) { + saving_filters = false; + updater.set({ filters_modified: false }); + show_alert({ + message: __('Filters saved'), + indicator: 'green' + }, 0.5); + }); + }, + add_card: function (updater, card_title, column_title) { + var doc = frappe.model.get_new_doc(this.doctype); + var field = this.card_meta.title_field; + var quick_entry = this.card_meta.quick_entry; + var board = this.board; + var state = this; + + if (field && !quick_entry) { + var doc_fields = {}; + doc_fields[field.fieldname] = card_title; + doc_fields[this.board.field_name] = column_title; + this.board.filters_array.forEach(function (f) { + if (f[2] !== "=") return; + doc_fields[f[1]] = f[3]; + }); + + if (quick_entry) { + frappe.route_options = {}; + $.extend(frappe.route_options, doc_fields); + frappe.new_doc(this.doctype, doc); + } else { + $.extend(doc, doc_fields); + return insert_doc(doc) + .then(function (r) { + var doc = r.message; + var card = prepare_card(doc, state, doc); + var cards = state.cards.slice(); + cards.push(card); + updater.set({ cards: cards }); + }); + } + } + }, + update_card: function (updater, card) { + var index = -1; + this.cards.forEach(function (c, i) { + if (c.name === card.name) { + index = i; + } + }); + var cards = this.cards.slice(); + if (index !== -1) { + cards.splice(index, 1, card); + } + updater.set({ cards: cards }); + }, + update_doc: function (updater, doc, card) { + var state = this; + return frappe.call({ + method: method_prefix + "update_doc", + args: { doc: doc }, + freeze: true + }).then(function (r) { + var updated_doc = r.message; + var updated_card = prepare_card(card, state, updated_doc); + fluxify.doAction('update_card', updated_card); + }); + }, + update_order: function(updater, order) { + return frappe.call({ + method: method_prefix + "update_order", + args: { + board_name: this.board.name, + order: order + } + }).then(function(r) { + var state = this; + var board = r.message[0]; + var updated_cards = r.message[1]; + var cards = update_cards_column(updated_cards); + var columns = prepare_columns(board.columns); + updater.set({ + cards: cards, + columns: columns + }); + }); + }, + update_column_order: function(updater, order) { + return frappe.call({ + method: method_prefix + "update_column_order", + args: { + board_name: this.board.name, + order: order + } + }).then(function(r) { + var board = r.message; + var columns = prepare_columns(board.columns); + updater.set({ + columns: columns + }); + }); + }, + set_indicator: function(updater, column, color) { + return frappe.call({ + method: method_prefix + "set_indicator", + args: { + board_name: this.board.name, + column_name: column.title, + indicator: color + } + }).then(function(r) { + var board = r.message; + var columns = prepare_columns(board.columns); + updater.set({ + columns: columns + }); + }) + } + } + }); + + frappe.views.KanbanBoard = function (opts) { + + var self = {}; + self.wrapper = opts.wrapper; + self.cur_list = opts.cur_list; + + function init() { + fluxify.doAction('init', opts) + store.on('change:columns', make_columns); + prepare(); + store.on('change:cur_list', setup_restore_columns); + store.on('change:columns', setup_restore_columns); + } + + function prepare() { + self.$kanban_board = $(frappe.render_template("kanban_board")); + self.$kanban_board.appendTo(self.wrapper); + self.$filter_area = self.cur_list.$page.find('.set-filters'); + bind_events(); + setup_sortable(); + } + + function make_columns() { + self.$kanban_board.find(".kanban-column").not(".add-new-column").remove(); + var columns = store.getState().columns; + + columns.filter(is_active_column).map(function (col) { + frappe.views.KanbanBoardColumn(col, self.$kanban_board); + }); + } + + function bind_events() { + bind_add_column(); + bind_save_filter(); + } + + function setup_sortable() { + var sortable = new Sortable(self.$kanban_board.get(0), { + group: 'columns', + animation: 150, + dataIdAttr: 'data-column-value', + filter: '.add-new-column', + handle: '.kanban-column-title', + onEnd: function(evt) { + var order = sortable.toArray(); + order = order.slice(1); + fluxify.doAction('update_column_order', order); + } + }); + } + + function bind_add_column() { + + var wrapper = self.$kanban_board; + var $add_new_column = self.$kanban_board.find(".add-new-column"), + $compose_column = $add_new_column.find(".compose-column"), + $compose_column_form = $add_new_column.find(".compose-column-form").hide(); + + $compose_column.on('click', function () { + $(this).hide(); + $compose_column_form.show(); + $compose_column_form.find('input').focus(); + }); + + //save on enter + $compose_column_form.keydown(function (e) { + if (e.which == 13) { + e.preventDefault(); + if (!frappe.request.ajax_count) { + // not already working -- double entry + var title = $compose_column_form.serializeArray()[0].value; + var col = { + title: title.trim() + } + fluxify.doAction('add_column', col); + $compose_column_form.find('input').val(''); + $compose_column.show(); + $compose_column_form.hide(); + } + } + }); + + // on form blur + $compose_column_form.find('input').on("blur", function (e) { + $(this).val(''); + $compose_column.show(); + $compose_column_form.hide(); + }); + } + + function bind_save_filter() { + var set_filter_state = function () { + fluxify.doAction('set_filter_state'); + } + + if(isBound(self.$kanban_board, 'after-refresh', set_filter_state)) return; + + store.on('change:filters_modified', function (modified) { + if(modified) fluxify.doAction('save_filters'); + }); + self.$kanban_board.on('after-refresh', set_filter_state); + } + + function setup_restore_columns() { + var cur_list = store.getState().cur_list; + var columns = store.getState().columns; + var list_row_right = cur_list.$page.find(`.list-row-head[data-list-renderer='Kanban'] .list-row-right`); + list_row_right.empty(); + + var archived_columns = columns.filter(function (col) { + return col.status === 'Archived'; + }); + + if (!archived_columns.length) return; + + var options = archived_columns.reduce(function (a, b) { + return a + "
  • " + + "" + + __(b.title) + "" + + "
  • "; + }, ""); + var $dropdown = $("") + + list_row_right.css("margin-top", 0).html($dropdown); + + $dropdown.find(".dropdown-menu").on("click", "button.restore-column", function (e) { + var column_title = $(this).data().column; + var col = { + title: column_title, + status: 'Archived' + } + fluxify.doAction('restore_column', col); + }); + } + + init(); + } + + frappe.views.KanbanBoardColumn = function (column, wrapper) { + var self = {}; + var filtered_cards = []; + + function init() { + make_dom(); + setup_sortable(); + make_cards(); + store.on('change:cards', make_cards); + bind_add_card(); + bind_options(); + } + + function make_dom() { + self.$kanban_column = $(frappe.render_template( + 'kanban_column', { + title: column.title, + doctype: store.getState().doctype, + indicator: column.indicator + })).appendTo(wrapper); + self.$kanban_cards = self.$kanban_column.find('.kanban-cards'); + } + + function make_cards() { + self.$kanban_cards.empty(); + var cards = store.getState().cards; + var board = store.getState().board; + filtered_cards = get_cards_for_column(cards, column); + + var order = column.order; + if(order) { + order = JSON.parse(order); + order.forEach(function(name) { + frappe.views.KanbanBoardCard(get_card(name), self.$kanban_cards); + }); + // new cards + filtered_cards.forEach(function(card) { + if(order.indexOf(card.name) === -1) { + frappe.views.KanbanBoardCard(card, self.$kanban_cards); + } + }); + } else { + filtered_cards.map(function (card) { + frappe.views.KanbanBoardCard(card, self.$kanban_cards); + }); + } + } + + function setup_sortable() { + var sortable = Sortable.create(self.$kanban_cards.get(0), { + group: "cards", + animation: 150, + dataIdAttr: 'data-name', + onStart: function (evt) { + wrapper.find('.kanban-card.add-card').fadeOut(200, function () { + wrapper.find('.kanban-cards').height('100vh'); + }); + }, + onEnd: function (evt) { + wrapper.find('.kanban-card.add-card').fadeIn(100); + wrapper.find('.kanban-cards').height('auto'); + // update order + var order = {} + wrapper.find('.kanban-column[data-column-value]') + .each(function() { + var col_name = $(this).data().columnValue; + order[col_name] = []; + $(this).find('.kanban-card-wrapper').each(function() { + var card_name = $(this).data().name; + order[col_name].push(card_name); + }); + }); + fluxify.doAction('update_order', order); + }, + onAdd: function (evt) { + }, + }); + } + + function bind_add_card() { + var $wrapper = self.$kanban_column; + var $btn_add = $wrapper.find('.add-card'); + var $new_card_area = $wrapper.find('.new-card-area'); + var $textarea = $new_card_area.find('textarea'); + + //Add card button + $new_card_area.hide(); + $btn_add.on('click', function () { + $btn_add.hide(); + $new_card_area.show(); + $textarea.focus(); + }); + + //save on enter + $new_card_area.keydown(function (e) { + if (e.which == 13) { + e.preventDefault(); + if (!frappe.request.ajax_count) { + // not already working -- double entry + e.preventDefault(); + var card_title = $textarea.val(); + fluxify.doAction('add_card', card_title, column.title); + $btn_add.show(); + $new_card_area.hide(); + $textarea.val(''); + } + } + }); + + // on textarea blur + $textarea.on("blur", function (e) { + $(this).val(''); + $btn_add.show(); + $new_card_area.hide(); + }); + } + + function bind_options() { + self.$kanban_column.find(".column-options .dropdown-menu") + .on("click", "[data-action]", function (e) { + var $btn = $(this); + var action = $btn.data().action; + + if (action === "archive") { + fluxify.doAction('archive_column', column); + } else if (action === "indicator") { + var color = $btn.data().indicator; + fluxify.doAction('set_indicator', column, color); + } + }); + get_column_indicators(function(indicators) { + var html = '
  • ' + html += indicators.reduce(function(prev, curr) { + return prev + '
    ' + }, ""); + html += '
  • '; + self.$kanban_column.find(".column-options .dropdown-menu") + .append(html); + }); + } + + init(); + } + + frappe.views.KanbanBoardCard = function (card, wrapper) { + var self = {}; + + function init() { + if(!card) return; + make_dom(); + render_card_meta(); + bind_edit_card(); + // edit_card_title(); + } + + function make_dom() { + var opts = { + name: card.name, + title: card.title + }; + self.$card = $(frappe.render_template('kanban_card', opts)) + .appendTo(wrapper); + } + + function render_card_meta() { + var html = ""; + if (card.comment_count > 0) + html += '' + + ' ' + card.comment_count + + ''; + html += get_assignees_html(); + self.$card.find(".kanban-card-meta").empty().append(html); + } + + function bind_edit_card() { + self.$card.find('.kanban-card.content').on('click', function () { + setup_edit_card(); + }); + } + + function setup_edit_card() { + if (self.edit_dialog) { + refresh_dialog(); + self.edit_dialog.show(); + return; + } + + var card_meta = store.getState().card_meta; + get_doc().then(function () { + // prepare dialog fields + var fields = []; + if (card_meta.description_field) { + fields.push({ + fieldtype: "Small Text", label: __("Description"), + fieldname: card_meta.description_field.fieldname + }); + } + + fields.push({ fieldtype: "Section Break" }); + fields.push({ + fieldtype: "Read Only", label: "Assigned to", + fieldname: "assignees" + }); + fields.push({ fieldtype: "Column Break" }); + + if (card_meta.due_date_field) { + fields.push(card_meta.due_date_field); + } + + var d = make_edit_dialog(card.title, fields); + + refresh_dialog(); + make_timeline(); + edit_card_title(); + + d.set_primary_action(__('Save'), function () { + if (d.working) return; + var doc = d.get_values(true); + $.extend(doc, { name: card.name, doctype: card.doctype }); + d.working = true; + fluxify.doAction('update_doc', doc, card) + .then(function (r) { + d.working = false; + d.hide(); + }); + }); + d.show(); + }); + } + + function refresh_dialog() { + set_dialog_fields(); + make_assignees(); + } + + function set_dialog_fields() { + self.edit_dialog.fields.forEach(function (df) { + var value = card.doc[df.fieldname]; + if (value) { + self.edit_dialog.set_value(df.fieldname, value); + } + }); + } + + function get_doc() { + return new Promise(function (resolve, reject) { + frappe.model.with_doc(card.doctype, card.name, function () { + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: card.doctype, + name: card.name + }, + callback: function (r) { + var doc = r.message; + if (!doc) { + reject(__("{0} {1} does not exist", [card.doctype, card.name])); + } + card.doc = doc; + resolve(); + } + }); + }); + }); + } + + function make_edit_dialog(title, fields) { + self.edit_dialog = new frappe.ui.Dialog({ + title: title, + fields: fields + }); + return self.edit_dialog; + } + + function make_assignees() { + var d = self.edit_dialog; + var html = get_assignees_html() + '\ + '; + + d.$wrapper.find("[data-fieldname='assignees'] .control-input-wrapper").empty().append(html); + d.$wrapper.find(".add-assignment").on("click", function () { + if (self.assign_to_dialog) { + self.assign_to_dialog.show(); + return; + } + show_assign_to_dialog(); + }); + } + + function get_assignees_html() { + return card.assigned_list.reduce(function (a, b) { + return a + frappe.avatar(b); + }, ""); + } + + function show_assign_to_dialog() { + self.dialog = new frappe.ui.form.AssignToDialog({ + obj: self, + method: 'frappe.desk.form.assign_to.add', + doctype: card.doctype, + docname: card.name, + callback: function(r) { + var user = self.assign_to_dialog.get_values().assign_to; + card.assigned_list.push(user); + fluxify.doAction('update_card', card); + refresh_dialog(); + } + }); + self.assign_to_dialog = self.dialog; + self.assign_to_dialog.show(); + } + + function make_timeline() { + var d = self.edit_dialog; + // timeline wrapper + d.$wrapper.find('.modal-body').append('
    '); + + // edit in full page button + $('') + .appendTo(d.$wrapper.find('.modal-body')) + .on('click', function () { + frappe.set_route("Form", card.doctype, card.name); + }); + var tl = new frappe.ui.form.Timeline({ + parent: d.$wrapper.find(".form-comments"), + frm: { + doctype: card.doctype, + docname: card.name, + get_docinfo: function () { + return frappe.model.get_docinfo(card.doctype, card.name) + }, + doc: card.doc, + sidebar: { + refresh_comments: function () { } + }, + trigger: function () { } + } + }); + tl.wrapper.addClass('in-dialog'); + tl.wrapper.find('.timeline-new-email').remove(); + // update comment count + var tl_refresh = tl.refresh.bind(tl); + tl.refresh = function () { + tl_refresh(); + var communications = tl.get_communications(); + var comment_count = communications.filter(function (c) { + return c.comment_type === 'Comment'; + }).length; + if (comment_count !== card.comment_count) { + card.comment_count = comment_count; + fluxify.doAction('update_card', card); + } + } + tl.refresh(); + } + + function edit_card_title() { + var $card_title = self.edit_dialog.header.find('.modal-title'); + var $title_wrapper = $card_title.parent(); + + $title_wrapper.addClass('edit-card-title').empty(); + + var template = repl('
    \ + %(card_title)s\ + \ +
    ', { card_title: card.title }); + + $title_wrapper.html(template); + + var $input = $title_wrapper.find('input').hide(); + var $span = $title_wrapper.find('span'); + + $span.on('click', function() { + $input.show(); + $span.hide(); + $input.val(card.title); + $input.focus(); + }); + + $input.on('blur', function() { + $input.hide(); + $span.show(); + }); + + $input.keydown(function(e) { + if (e.which === 13) { + e.preventDefault(); + var new_title = $input.val(); + if (card.title === new_title) { + return; + } + get_doc().then(function () { + var tf = store.getState().card_meta.title_field.fieldname; + var doc = card.doc; + doc[tf] = new_title; + fluxify.doAction('update_doc', doc, card); + $span.html(new_title); + $input.trigger('blur'); + }) + } + }) + } + + function edit_card_title_old() { + + self.$card.find('.kanban-card-edit').on('click', function (e) { + e.stopPropagation(); + $edit_card_area.show(); + $kanban_card_area.hide(); + $textarea.focus(); + }); + + $textarea.on('blur', function () { + $edit_card_area.hide(); + $kanban_card_area.show(); + }); + + $textarea.keydown(function (e) { + if (e.which === 13) { + e.preventDefault(); + var new_title = $textarea.val(); + if (card.title === new_title) { + return; + } + get_doc().then(function () { + var tf = store.getState().card_meta.title_field.fieldname; + var doc = card.doc; + doc[tf] = new_title; + fluxify.doAction('update_doc', doc, card); + }) + } + }) + } + + init(); + } + + // Helpers + function get_board(board_name) { + return frappe.call({ + type: 'GET', + method: "frappe.client.get", + args: { + doctype: 'Kanban Board', + name: board_name + } + }).then(function(r) { + var board = r.message; + if (!board) { + frappe.msgprint(__('Kanban Board {0} does not exist.', + ['' + self.board_name + ''])); + } + return prepare_board(board); + }, function(e) { + console.log(e) + }); + } + + function prepare_board(board) { + board.filters_array = board.filters ? + JSON.parse(board.filters) : []; + return board; + } + + function get_card_meta(opts) { + var meta = frappe.get_meta(opts.doctype); + var doc = frappe.model.get_new_doc(opts.doctype); + var title_field = null; + var quick_entry = false; + var description_field = null; + var due_date_field = null; + + if(meta.title_field) { + title_field = frappe.meta.get_field(opts.doctype, meta.title_field); + } + + meta.fields.forEach(function (df) { + if (in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) && !title_field) { + // can be mapped to textarea + title_field = df; + } + if (df.fieldtype === "Text Editor" && !description_field) { + description_field = df; + } + if (!due_date_field) { + due_date_field = get_date_field(meta.fields); + } + }); + + // quick entry + var mandatory = meta.fields.filter(function(df) { + return df.reqd && !doc[df.fieldname]; + }); + if(mandatory.length > 1) { + quick_entry = true; + } + + if(!title_field) { + title_field = frappe.meta.get_field(opts.doctype, 'name'); + } + + return { + quick_entry: quick_entry, + title_field: title_field, + description_field: description_field, + due_date_field: due_date_field, + } + } + + function get_date_field(fields) { + var filtered = fields.filter(function(df) { + return df.fieldtype === 'Date' && + df.fieldname.indexOf('date') !== -1; + }); + var field = filtered.find(function(df) { + return df.fieldname.indexOf('end') !== -1; + }); + return field || filtered[0]; + } + + function prepare_card(card, state, doc) { + var assigned_list = card._assign ? + JSON.parse(card._assign) : []; + var comment_count = card._comment_count || 0; + + if (doc) { + card = Object.assign({}, card, doc); + } + + return { + doctype: state.doctype, + name: card.name, + title: card[state.card_meta.title_field.fieldname], + column: card[state.board.field_name], + assigned_list: card.assigned_list || assigned_list, + comment_count: card.comment_count || comment_count, + doc: doc + }; + } + + function prepare_columns(columns) { + return columns.map(function (col) { + return { + title: col.column_name, + status: col.status, + order: col.order, + indicator: col.indicator || 'darkgrey' + }; + }); + } + + function modify_column_field_in_c11n(doc, board, title, action) { + doc.fields.forEach(function (df) { + if (df.fieldname === board.field_name && df.fieldtype === "Select") { + if (action === "add") { + //add column_name to Select field's option field + df.options += "\n" + title; + } else if (action === "delete") { + var options = df.options.split("\n"); + var index = options.indexOf(title); + if (index !== -1) options.splice(index, 1); + df.options = options.join("\n"); + } + } + }); + return doc; + } + + function fetch_customization(doctype) { + return new Promise(function (resolve, reject) { + frappe.model.with_doc("Customize Form", "Customize Form", function () { + var doc = frappe.get_doc("Customize Form"); + doc.doc_type = doctype; + frappe.call({ + doc: doc, + method: "fetch_to_customize", + callback: function (r) { + resolve(r.docs[0]); + } + }); + }); + }); + } + + function save_customization(doc) { + if (!doc) return; + doc.hide_success = true; + return frappe.call({ + doc: doc, + method: "save_customization" + }); + } + + function insert_doc(doc) { + return frappe.call({ + method: "frappe.client.insert", + args: { + doc: doc + }, + callback: function (r) { + frappe.model.clear_doc(doc.doctype, doc.name); + show_alert({ message: __("Saved"), indicator: 'green' }, 1); + } + }); + } + + function update_kanban_board(board_name, column_title, action) { + var method; + var args = { + board_name: board_name, + column_title: column_title + }; + if (action === 'add') { + method = 'add_column'; + } else if (action === 'archive' || action === 'restore') { + method = 'archive_restore_column'; + args.status = action === 'archive' ? 'Archived' : 'Active'; + } + return frappe.call({ + method: method_prefix + method, + args: args + }); + } + + function is_filters_modified(board, cur_list) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); + resolve(list_filters !== board.filters); + }, 2000); + }) + } + + function is_active_column(col) { + return col.status !== 'Archived' + } + + function get_cards_for_column(cards, column) { + return cards.filter(function (card) { + return card.column === column.title + }); + } + + function get_card(name) { + return store.getState().cards.find(function (c) { + return c.name === name; + }); + } + + function update_cards_column(updated_cards) { + var cards = store.getState().cards; + cards.forEach(function(c) { + updated_cards.forEach(function(uc) { + if(uc.name === c.name) { + c.column = uc.column; + } + }); + }); + return cards; + } + + function get_column_indicators(callback) { + frappe.model.with_doctype('Kanban Board Column', function() { + var meta = frappe.get_meta('Kanban Board Column'); + var indicators; + meta.fields.forEach(function(df) { + if(df.fieldname==='indicator') { + indicators = df.options.split("\n"); + } + }); + if(!indicators) { + // + indicators = ['green', 'blue', 'orange', 'grey'] + } + callback(indicators); + }); + } + + function isBound(el, event, fn) { + var events = $._data(el[0], 'events'); + if(!events) return false; + var handlers = events[event]; + var flag = false; + handlers.forEach(function(h) { + if(h.handler.name === fn.name) + flag = true; + }); + return flag; + } +})(); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index c75562fa2a..4391202dfd 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -1,1033 +1,69 @@ -frappe.provide("frappe.views"); - -(function () { - - var method_prefix = 'frappe.desk.doctype.kanban_board.kanban_board.'; - - var store = fluxify.createStore({ - id: 'store', - initialState: { - doctype: '', - board: {}, - card_meta: {}, - cards: [], - columns: [], - filters_modified: false, - cur_list: {} - }, - actionCallbacks: { - init: function (updater, opts) { - get_board(opts.board_name) - .then(function (board) { - var card_meta = get_card_meta(opts); - opts.card_meta = card_meta; - opts.board = board; - var cards = opts.cards.map(function (card) { - return prepare_card(card, opts); - }); - var columns = prepare_columns(board.columns); - - updater.set({ - doctype: opts.doctype, - board: board, - card_meta: card_meta, - cards: cards, - columns: columns, - cur_list: opts.cur_list - }); - }); - }, - add_column: function (updater, col) { - fluxify.doAction('update_column', col, 'add'); - }, - archive_column: function (updater, col) { - fluxify.doAction('update_column', col, 'archive'); - }, - restore_column: function (updater, col) { - fluxify.doAction('update_column', col, 'restore'); - }, - update_column: function (updater, col, action) { - var doctype = this.doctype; - var board = this.board; - fetch_customization(doctype) - .then(function (doc) { - return modify_column_field_in_c11n(doc, board, col.title, action) - }) - .then(save_customization) - .then(function (r) { - return update_kanban_board(board.name, col.title, action) - }).then(function (r) { - var cols = r.message; - updater.set({ - columns: prepare_columns(cols) - }); - }, function (err) { - console.error(err); - }); - }, - set_filter_state: function (updater) { - is_filters_modified(this.board, this.cur_list) - .then(function(flag) { - updater.set({ - filters_modified: flag - }); - }); - }, - save_filters: function (updater) { - var filters = JSON.stringify(this.cur_list.filter_list.get_filters()); - frappe.call({ - method: method_prefix + 'save_filters', - args: { - board_name: this.board.name, - filters: filters - } - }).then(function(r) { - updater.set({ filters_modified: false }); - show_alert({ - message: __('Filters saved'), - indicator: 'green' - }, 0.5); - }); - }, - add_card: function (updater, card_title, column_title) { - var doc = frappe.model.get_new_doc(this.doctype); - var field = this.card_meta.title_field; - var quick_entry = this.card_meta.quick_entry; - var board = this.board; - var state = this; - - if (field && !quick_entry) { - var doc_fields = {}; - doc_fields[field.fieldname] = card_title; - doc_fields[this.board.field_name] = column_title; - this.board.filters_array.forEach(function (f) { - if (f[2] !== "=") return; - doc_fields[f[1]] = f[3]; - }); - - if (quick_entry) { - frappe.route_options = {}; - $.extend(frappe.route_options, doc_fields); - frappe.new_doc(this.doctype, doc); - } else { - $.extend(doc, doc_fields); - return insert_doc(doc) - .then(function (r) { - var doc = r.message; - var card = prepare_card(doc, state, doc); - var cards = state.cards.slice(); - cards.push(card); - updater.set({ cards: cards }); - }); - } - } - }, - update_card: function (updater, card) { - var index = -1; - this.cards.forEach(function (c, i) { - if (c.name === card.name) { - index = i; - } - }); - var cards = this.cards.slice(); - if (index !== -1) { - cards.splice(index, 1, card); - } - updater.set({ cards: cards }); - }, - update_doc: function (updater, doc, card) { - var state = this; - return frappe.call({ - method: method_prefix + "update_doc", - args: { doc: doc }, - freeze: true - }).then(function (r) { - var updated_doc = r.message; - var updated_card = prepare_card(card, state, updated_doc); - fluxify.doAction('update_card', updated_card); - }); - }, - update_order: function(updater, order) { - return frappe.call({ - method: method_prefix + "update_order", - args: { - board_name: this.board.name, - order: order - } - }).then(function(r) { - var state = this; - var board = r.message[0]; - var updated_cards = r.message[1]; - var cards = update_cards_column(updated_cards); - var columns = prepare_columns(board.columns); - updater.set({ - cards: cards, - columns: columns - }); - }); - }, - update_column_order: function(updater, order) { - return frappe.call({ - method: method_prefix + "update_column_order", - args: { - board_name: this.board.name, - order: order - } - }).then(function(r) { - var board = r.message; - var columns = prepare_columns(board.columns); - updater.set({ - columns: columns - }); - }); - }, - set_indicator: function(updater, column, color) { - return frappe.call({ - method: method_prefix + "set_indicator", - args: { - board_name: this.board.name, - column_name: column.title, - indicator: color - } - }).then(function(r) { - var board = r.message; - var columns = prepare_columns(board.columns); - updater.set({ - columns: columns - }); - }) - } - } - }); - - frappe.views.KanbanBoard = function (opts) { - - var self = {}; - self.wrapper = opts.wrapper; - self.cur_list = opts.cur_list; - - function init() { - fluxify.doAction('init', opts) - store.on('change:columns', make_columns); - prepare(); - store.on('change:cur_list', setup_restore_columns); - store.on('change:columns', setup_restore_columns); - } - - function prepare() { - self.$kanban_board = $(frappe.render_template("kanban_board")); - self.$kanban_board.appendTo(self.wrapper); - self.$filter_area = self.cur_list.$page.find('.set-filters'); - bind_events(); - setup_sortable(); - } - - function make_columns() { - self.$kanban_board.find(".kanban-column").not(".add-new-column").remove(); - var columns = store.getState().columns; - - columns.filter(is_active_column).map(function (col) { - frappe.views.KanbanBoardColumn(col, self.$kanban_board); - }); - } - - function bind_events() { - bind_add_column(); - bind_save_filter(); - } - - function setup_sortable() { - var sortable = new Sortable(self.$kanban_board.get(0), { - group: 'columns', - animation: 150, - dataIdAttr: 'data-column-value', - filter: '.add-new-column', - handle: '.kanban-column-title', - onEnd: function(evt) { - var order = sortable.toArray(); - order = order.slice(1); - fluxify.doAction('update_column_order', order); - } - }); - } - - function bind_add_column() { - - var wrapper = self.$kanban_board; - var $add_new_column = self.$kanban_board.find(".add-new-column"), - $compose_column = $add_new_column.find(".compose-column"), - $compose_column_form = $add_new_column.find(".compose-column-form").hide(); - - $compose_column.on('click', function () { - $(this).hide(); - $compose_column_form.show(); - $compose_column_form.find('input').focus(); - }); - - //save on enter - $compose_column_form.keydown(function (e) { - if (e.which == 13) { - e.preventDefault(); - if (!frappe.request.ajax_count) { - // not already working -- double entry - var title = $compose_column_form.serializeArray()[0].value; - var col = { - title: title.trim() - } - fluxify.doAction('add_column', col); - $compose_column_form.find('input').val(''); - $compose_column.show(); - $compose_column_form.hide(); - } - } - }); - - // on form blur - $compose_column_form.find('input').on("blur", function (e) { - $(this).val(''); - $compose_column.show(); - $compose_column_form.hide(); - }); - } - - function bind_save_filter() { - var set_filter_state = function () { - fluxify.doAction('set_filter_state'); - } - if(isBound(self.cur_list.wrapper, 'render-complete', set_filter_state)) return; - - store.on('change:filters_modified', function (modified) { - if(modified) fluxify.doAction('save_filters'); - }); - self.cur_list.wrapper.on('render-complete', set_filter_state); - } - - function setup_restore_columns() { - var cur_list = store.getState().cur_list; - var columns = store.getState().columns; - cur_list.$page.find(".list-row-right").empty(); - - var archived_columns = columns.filter(function (col) { - return col.status === 'Archived'; - }); - - if (!archived_columns.length) return; - - var options = archived_columns.reduce(function (a, b) { - return a + "
  • " + - "" + - __(b.title) + "" + - "
  • "; - }, ""); - var $dropdown = $("") - - cur_list.$page.find(".list-row-right").css("margin-top", 0).html($dropdown); - - $dropdown.find(".dropdown-menu").on("click", "button.restore-column", function (e) { - var column_title = $(this).data().column; - var col = { - title: column_title, - status: 'Archived' - } - fluxify.doAction('restore_column', col); - }); - } - - init(); - } - - frappe.views.KanbanBoardColumn = function (column, wrapper) { - var self = {}; - var filtered_cards = []; - - function init() { - make_dom(); - setup_sortable(); - make_cards(); - store.on('change:cards', make_cards); - bind_add_card(); - bind_options(); - } - - function make_dom() { - self.$kanban_column = $(frappe.render_template( - 'kanban_column', { - title: column.title, - doctype: store.getState().doctype, - indicator: column.indicator - })).appendTo(wrapper); - self.$kanban_cards = self.$kanban_column.find('.kanban-cards'); - } - - function make_cards() { - self.$kanban_cards.empty(); - var cards = store.getState().cards; - var board = store.getState().board; - filtered_cards = get_cards_for_column(cards, column); - - var order = column.order; - if(order) { - order = JSON.parse(order); - order.forEach(function(name) { - frappe.views.KanbanBoardCard(get_card(name), self.$kanban_cards); - }); - // new cards - filtered_cards.forEach(function(card) { - if(order.indexOf(card.name) === -1) { - frappe.views.KanbanBoardCard(card, self.$kanban_cards); - } - }); - } else { - filtered_cards.map(function (card) { - frappe.views.KanbanBoardCard(card, self.$kanban_cards); - }); - } - } - - function setup_sortable() { - var sortable = Sortable.create(self.$kanban_cards.get(0), { - group: "cards", - animation: 150, - dataIdAttr: 'data-name', - onStart: function (evt) { - wrapper.find('.kanban-card.add-card').fadeOut(200, function () { - wrapper.find('.kanban-cards').height('100vh'); - }); - }, - onEnd: function (evt) { - wrapper.find('.kanban-card.add-card').fadeIn(100); - wrapper.find('.kanban-cards').height('auto'); - // update order - var order = {} - wrapper.find('.kanban-column[data-column-value]') - .each(function() { - var col_name = $(this).data().columnValue; - order[col_name] = []; - $(this).find('.kanban-card-wrapper').each(function() { - var card_name = $(this).data().name; - order[col_name].push(card_name); - }); - }); - fluxify.doAction('update_order', order); - }, - onAdd: function (evt) { - }, - }); - } - - function bind_add_card() { - var $wrapper = self.$kanban_column; - var $btn_add = $wrapper.find('.add-card'); - var $new_card_area = $wrapper.find('.new-card-area'); - var $textarea = $new_card_area.find('textarea'); - - //Add card button - $new_card_area.hide(); - $btn_add.on('click', function () { - $btn_add.hide(); - $new_card_area.show(); - $textarea.focus(); - }); - - //save on enter - $new_card_area.keydown(function (e) { - if (e.which == 13) { - e.preventDefault(); - if (!frappe.request.ajax_count) { - // not already working -- double entry - e.preventDefault(); - var card_title = $textarea.val(); - fluxify.doAction('add_card', card_title, column.title); - $btn_add.show(); - $new_card_area.hide(); - $textarea.val(''); - } - } - }); - - // on textarea blur - $textarea.on("blur", function (e) { - $(this).val(''); - $btn_add.show(); - $new_card_area.hide(); - }); - } - - function bind_options() { - self.$kanban_column.find(".column-options .dropdown-menu") - .on("click", "[data-action]", function (e) { - var $btn = $(this); - var action = $btn.data().action; - - if (action === "archive") { - fluxify.doAction('archive_column', column); - } else if (action === "indicator") { - var color = $btn.data().indicator; - fluxify.doAction('set_indicator', column, color); - } - }); - get_column_indicators(function(indicators) { - var html = '
  • ' - html += indicators.reduce(function(prev, curr) { - return prev + '
    ' - }, ""); - html += '
  • '; - self.$kanban_column.find(".column-options .dropdown-menu") - .append(html); - }); - } - - init(); - } - - frappe.views.KanbanBoardCard = function (card, wrapper) { - var self = {}; - - function init() { - if(!card) return; - make_dom(); - render_card_meta(); - bind_edit_card(); - // edit_card_title(); - } - - function make_dom() { - var opts = { - name: card.name, - title: card.title - }; - self.$card = $(frappe.render_template('kanban_card', opts)) - .appendTo(wrapper); - } - - function render_card_meta() { - var html = ""; - if (card.comment_count > 0) - html += '' + - ' ' + card.comment_count + - ''; - html += get_assignees_html(); - self.$card.find(".kanban-card-meta").empty().append(html); - } - - function bind_edit_card() { - self.$card.find('.kanban-card.content').on('click', function () { - setup_edit_card(); - }); - } - - function setup_edit_card() { - if (self.edit_dialog) { - refresh_dialog(); - self.edit_dialog.show(); - return; - } - - var card_meta = store.getState().card_meta; - get_doc().then(function () { - // prepare dialog fields - var fields = []; - if (card_meta.description_field) { - fields.push({ - fieldtype: "Small Text", label: __("Description"), - fieldname: card_meta.description_field.fieldname - }); - } - - fields.push({ fieldtype: "Section Break" }); - fields.push({ - fieldtype: "Read Only", label: "Assigned to", - fieldname: "assignees" - }); - fields.push({ fieldtype: "Column Break" }); - - if (card_meta.due_date_field) { - fields.push(card_meta.due_date_field); - } - - var d = make_edit_dialog(card.title, fields); - - refresh_dialog(); - make_timeline(); - edit_card_title(); - - d.set_primary_action(__('Save'), function () { - if (d.working) return; - var doc = d.get_values(true); - $.extend(doc, { name: card.name, doctype: card.doctype }); - d.working = true; - fluxify.doAction('update_doc', doc, card) - .then(function (r) { - d.working = false; - d.hide(); - }); - }); - d.show(); - }); - } - - function refresh_dialog() { - set_dialog_fields(); - make_assignees(); - } - - function set_dialog_fields() { - self.edit_dialog.fields.forEach(function (df) { - var value = card.doc[df.fieldname]; - if (value) { - self.edit_dialog.set_value(df.fieldname, value); - } - }); - } - - function get_doc() { - return new Promise(function (resolve, reject) { - frappe.model.with_doc(card.doctype, card.name, function () { - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: card.doctype, - name: card.name - }, - callback: function (r) { - var doc = r.message; - if (!doc) { - reject(__("{0} {1} does not exist", [card.doctype, card.name])); - } - card.doc = doc; - resolve(); - } - }); - }); - }); - } - - function make_edit_dialog(title, fields) { - self.edit_dialog = new frappe.ui.Dialog({ - title: title, - fields: fields - }); - return self.edit_dialog; - } - - function make_assignees() { - var d = self.edit_dialog; - var html = get_assignees_html() + '\ - '; - - d.$wrapper.find("[data-fieldname='assignees'] .control-input-wrapper").empty().append(html); - d.$wrapper.find(".add-assignment").on("click", function () { - if (self.assign_to_dialog) { - self.assign_to_dialog.show(); - return; - } - show_assign_to_dialog(); - }); - } - - function get_assignees_html() { - return card.assigned_list.reduce(function (a, b) { - return a + frappe.avatar(b); - }, ""); - } - - function show_assign_to_dialog() { - self.dialog = new frappe.ui.form.AssignToDialog({ - obj: self, - method: 'frappe.desk.form.assign_to.add', - doctype: card.doctype, - docname: card.name, - callback: function(r) { - var user = self.assign_to_dialog.get_values().assign_to; - card.assigned_list.push(user); - fluxify.doAction('update_card', card); - refresh_dialog(); - } - }); - self.assign_to_dialog = self.dialog; - self.assign_to_dialog.show(); - } - - function make_timeline() { - var d = self.edit_dialog; - // timeline wrapper - d.$wrapper.find('.modal-body').append('
    '); - - // edit in full page button - $('') - .appendTo(d.$wrapper.find('.modal-body')) - .on('click', function () { - frappe.set_route("Form", card.doctype, card.name); - }); - var tl = new frappe.ui.form.Timeline({ - parent: d.$wrapper.find(".form-comments"), - frm: { - doctype: card.doctype, - docname: card.name, - get_docinfo: function () { - return frappe.model.get_docinfo(card.doctype, card.name) - }, - doc: card.doc, - sidebar: { - refresh_comments: function () { } - }, - trigger: function () { } - } - }); - tl.wrapper.addClass('in-dialog'); - tl.wrapper.find('.timeline-new-email').remove(); - // update comment count - var tl_refresh = tl.refresh.bind(tl); - tl.refresh = function () { - tl_refresh(); - var communications = tl.get_communications(); - var comment_count = communications.filter(function (c) { - return c.comment_type === 'Comment'; - }).length; - if (comment_count !== card.comment_count) { - card.comment_count = comment_count; - fluxify.doAction('update_card', card); - } - } - tl.refresh(); - } - - function edit_card_title() { - var $card_title = self.edit_dialog.header.find('.modal-title'); - var $title_wrapper = $card_title.parent(); - - $title_wrapper.addClass('edit-card-title').empty(); - - var template = repl('
    \ - %(card_title)s\ - \ -
    ', { card_title: card.title }); - - $title_wrapper.html(template); - - var $input = $title_wrapper.find('input').hide(); - var $span = $title_wrapper.find('span'); - - $span.on('click', function() { - $input.show(); - $span.hide(); - $input.val(card.title); - $input.focus(); - }); - - $input.on('blur', function() { - $input.hide(); - $span.show(); - }); - - $input.keydown(function(e) { - if (e.which === 13) { - e.preventDefault(); - var new_title = $input.val(); - if (card.title === new_title) { - return; - } - get_doc().then(function () { - var tf = store.getState().card_meta.title_field.fieldname; - var doc = card.doc; - doc[tf] = new_title; - fluxify.doAction('update_doc', doc, card); - $span.html(new_title); - $input.trigger('blur'); - }) - } - }) - } - - function edit_card_title_old() { - - self.$card.find('.kanban-card-edit').on('click', function (e) { - e.stopPropagation(); - $edit_card_area.show(); - $kanban_card_area.hide(); - $textarea.focus(); - }); - - $textarea.on('blur', function () { - $edit_card_area.hide(); - $kanban_card_area.show(); - }); - - $textarea.keydown(function (e) { - if (e.which === 13) { - e.preventDefault(); - var new_title = $textarea.val(); - if (card.title === new_title) { - return; - } - get_doc().then(function () { - var tf = store.getState().card_meta.title_field.fieldname; - var doc = card.doc; - doc[tf] = new_title; - fluxify.doAction('update_doc', doc, card); - }) - } - }) - } - - init(); - } - - // Helpers - function get_board(board_name) { - return frappe.call({ - method: "frappe.client.get", - args: { - doctype: 'Kanban Board', - name: board_name - } - }).then(function(r) { - var board = r.message; - if (!board) { - frappe.msgprint(__('Kanban Board {0} does not exist.', - ['' + self.board_name + ''])); - } - return prepare_board(board); - }); - } - - function prepare_board(board) { - board.filters_array = board.filters ? - JSON.parse(board.filters) : []; - return board; - } - - function get_card_meta(opts) { - var meta = frappe.get_meta(opts.doctype); - var doc = frappe.model.get_new_doc(opts.doctype); - var title_field = null; - var quick_entry = false; - var description_field = null; - var due_date_field = null; - - if(meta.title_field) { - title_field = frappe.meta.get_field(opts.doctype, meta.title_field); - } - - meta.fields.forEach(function (df) { - if (in_list(['Data', 'Text', 'Small Text', 'Text Editor'], df.fieldtype) && !title_field) { - // can be mapped to textarea - title_field = df; - } - if (df.fieldtype === "Text Editor" && !description_field) { - description_field = df; - } - if (!due_date_field) { - due_date_field = get_date_field(meta.fields); - } - }); - - // quick entry - var mandatory = meta.fields.filter(function(df) { - return df.reqd && !doc[df.fieldname]; - }); - if(mandatory.length > 1) { - quick_entry = true; - } - - if(!title_field) { - title_field = frappe.meta.get_field(opts.doctype, 'name'); - } - - return { - quick_entry: quick_entry, - title_field: title_field, - description_field: description_field, - due_date_field: due_date_field, - } - } - - function get_date_field(fields) { - var filtered = fields.filter(function(df) { - return df.fieldtype === 'Date' && - df.fieldname.indexOf('date') !== -1; - }); - var field = filtered.find(function(df) { - return df.fieldname.indexOf('end') !== -1; - }); - return field || filtered[0]; - } - - function prepare_card(card, state, doc) { - var assigned_list = card._assign ? - JSON.parse(card._assign) : []; - var comment_count = card._comment_count || 0; - - if (doc) { - card = Object.assign({}, card, doc); - } - - return { - doctype: state.doctype, - name: card.name, - title: card[state.card_meta.title_field.fieldname], - column: card[state.board.field_name], - assigned_list: card.assigned_list || assigned_list, - comment_count: card.comment_count || comment_count, - doc: doc - }; - } - - function prepare_columns(columns) { - return columns.map(function (col) { - return { - title: col.column_name, - status: col.status, - order: col.order, - indicator: col.indicator || 'darkgrey' - }; - }); - } - - function modify_column_field_in_c11n(doc, board, title, action) { - doc.fields.forEach(function (df) { - if (df.fieldname === board.field_name && df.fieldtype === "Select") { - if (action === "add") { - //add column_name to Select field's option field - df.options += "\n" + title; - } else if (action === "delete") { - var options = df.options.split("\n"); - var index = options.indexOf(title); - if (index !== -1) options.splice(index, 1); - df.options = options.join("\n"); - } - } - }); - return doc; - } - - function fetch_customization(doctype) { - return new Promise(function (resolve, reject) { - frappe.model.with_doc("Customize Form", "Customize Form", function () { - var doc = frappe.get_doc("Customize Form"); - doc.doc_type = doctype; - frappe.call({ - doc: doc, - method: "fetch_to_customize", - callback: function (r) { - resolve(r.docs[0]); - } - }); - }); - }); - } - - function save_customization(doc) { - if (!doc) return; - doc.hide_success = true; - return frappe.call({ - doc: doc, - method: "save_customization" - }); - } - - function insert_doc(doc) { - return frappe.call({ - method: "frappe.client.insert", - args: { - doc: doc - }, - callback: function (r) { - frappe.model.clear_doc(doc.doctype, doc.name); - show_alert({ message: __("Saved"), indicator: 'green' }, 1); - } - }); - } - - function update_kanban_board(board_name, column_title, action) { - var method; - var args = { +frappe.provide('frappe.views'); + +frappe.views.KanbanView = frappe.views.ListRenderer.extend({ + name: 'Kanban', + render_view: function(values) { + var board_name = this.get_board_name(); + this.kanban = new frappe.views.KanbanBoard({ + doctype: this.doctype, board_name: board_name, - column_title: column_title - }; - if (action === 'add') { - method = 'add_column'; - } else if (action === 'archive' || action === 'restore') { - method = 'archive_restore_column'; - args.status = action === 'archive' ? 'Archived' : 'Active'; - } - return frappe.call({ - method: method_prefix + method, - args: args - }); - } - - function is_filters_modified(board, cur_list) { - return new Promise(function(resolve, reject) { - setTimeout(function() { - var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); - resolve(list_filters !== board.filters); - }, 2000); - }) - } - - function is_active_column(col) { - return col.status !== 'Archived' - } - - function get_cards_for_column(cards, column) { - return cards.filter(function (card) { - return card.column === column.title - }); - } - - function get_card(name) { - return store.getState().cards.find(function (c) { - return c.name === name; - }); - } - - function update_cards_column(updated_cards) { - var cards = store.getState().cards; - cards.forEach(function(c) { - updated_cards.forEach(function(uc) { - if(uc.name === c.name) { - c.column = uc.column; - } - }); - }); - return cards; - } - - function get_column_indicators(callback) { - frappe.model.with_doctype('Kanban Board Column', function() { - var meta = frappe.get_meta('Kanban Board Column'); - var indicators; - meta.fields.forEach(function(df) { - if(df.fieldname==='indicator') { - indicators = df.options.split("\n"); - } - }); - if(!indicators) { - // - indicators = ['green', 'blue', 'orange', 'grey'] - } - callback(indicators); - }); - } - - function isBound(el, event, fn) { - var events = $._data(el[0], 'events'); - var handlers = events[event]; - var flag = false; - handlers.forEach(function(h) { - if(h.handler.name === fn.name) - flag = true; - }); - return flag; - } -})(); \ No newline at end of file + cards: values, + wrapper: this.wrapper, + cur_list: this.list_view, + user_settings: this.user_settings + }); + }, + after_refresh: function() { + this.wrapper.find('.kanban').trigger('after-refresh'); + frappe.kanban_filters[this.get_board_name()] = + this.list_view.filter_list.get_filters(); + }, + should_refresh: function() { + var to_refresh = this._super(); + if(!to_refresh) { + this.last_kanban_board = this.current_kanban_board || ''; + this.current_kanban_board = this.get_board_name(); + this.page_title = __(this.get_board_name()); + + to_refresh = this.current_kanban_board !== this.last_kanban_board; + } + return to_refresh; + }, + init_settings: function() { + this._super(); + this.filters = this.get_kanban_filters(); + }, + get_kanban_filters: function() { + frappe.provide('frappe.kanban_filters'); + + var board_name = this.get_board_name(); + if (!frappe.kanban_filters[board_name]) { + var kb = this.meta.__kanban_boards.find( + board => board.name === board_name + ); + frappe.kanban_filters[board_name] = JSON.parse(kb && kb.filters || "[]"); + } + var filters = frappe.kanban_filters[board_name]; + return filters; + }, + set_defaults: function() { + this._super(); + this.no_realtime = true; + this.page_title = __(this.get_board_name()); + }, + get_board_name: function() { + var route = frappe.get_route(); + if(!route[3] || !this.meta.__kanban_boards.find(b => b.name === route[3])) { + frappe.throw(__(`Kanban Board ${route[3] || ''} not found`)); + return; + } + return route[3]; + }, + get_header_html: function() { + return frappe.render_template('list_item_row_head', { main: '', list: this }); + }, + required_libs: [ + 'assets/frappe/js/frappe/views/kanban/fluxify.min.js', + 'assets/frappe/js/frappe/views/kanban/kanban_board.js' + ] +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/reports/reportview.js b/frappe/public/js/frappe/views/reports/reportview.js index a9f31df423..c15ae2e2a5 100644 --- a/frappe/public/js/frappe/views/reports/reportview.js +++ b/frappe/public/js/frappe/views/reports/reportview.js @@ -62,7 +62,7 @@ frappe.views.ReportViewPage = Class.extend({ } }); -frappe.views.ReportView = frappe.ui.Listing.extend({ +frappe.views.ReportView = frappe.ui.BaseList.extend({ init: function(opts) { var me = this; $.extend(this, opts); @@ -79,11 +79,11 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ this._body = $('
    ').appendTo(this.page.main); this.page_title = __('Report')+ ': ' + __(this.docname ? (this.doctype + ' - ' + this.docname) : this.doctype); this.page.set_title(this.page_title); - this.init_list_settings(); + this.init_user_settings(); this.make({ page: this.parent.page, method: 'frappe.desk.reportview.get', - save_list_settings: true, + save_user_settings: true, get_args: this.get_args, parent: this._body, start: 0, @@ -139,8 +139,8 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ // pre-select mandatory columns var me = this; var columns = []; - if(this.list_settings.fields && !this.docname) { - this.list_settings.fields.forEach(function(field) { + if(this.user_settings.fields && !this.docname) { + this.user_settings.fields.forEach(function(field) { var coldef = me.get_column_info_from_field(field); if(!in_list(['_seen', '_comments', '_user_tags', '_assign', '_liked_by', 'docstatus'], coldef[0])) { columns.push(coldef); @@ -226,21 +226,21 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ set_route_filters: function(first_load) { var me = this; - if(frappe.route_options && !this.list_settings.filters) { + if(frappe.route_options && !this.user_settings.filters) { this.set_filters_from_route_options(); return true; - } else if(this.list_settings - && this.list_settings.filters + } else if(this.user_settings + && this.user_settings.filters && !this.docname - && (this.list_settings.updated_on != this.list_settings_updated_on)) { + && (this.user_settings.updated_on != this.user_settings_updated_on)) { // list settings (previous settings) this.filter_list.clear_filters(); - $.each(this.list_settings.filters, function(i, f) { + $.each(this.user_settings.filters, function(i, f) { me.filter_list.add_filter(f[0], f[1], f[2], f[3]); }); return true; } - this.list_settings_updated_on = this.list_settings.updated_on; + this.user_settings_updated_on = this.user_settings.updated_on; }, setup_print: function() { @@ -263,7 +263,7 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ order_by: this.get_order_by(), add_total_row: this.add_total_row, filters: this.filter_list.get_filters(), - save_list_settings_fields: 1, + save_user_settings_fields: 1, with_childnames: 1, file_format_type: this.file_format_type } @@ -378,7 +378,7 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ }, // render data - render_list: function() { + render_view: function() { var me = this; var data = this.get_unique_data(this.column_info); @@ -598,7 +598,7 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ this.page.add_inner_button(__('Show Totals'), function() { me.add_totals_row = 1 - (me.add_totals_row ? me.add_totals_row : 0); - me.render_list(); + me.render_view(); }); }, diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index d971052cb2..e7c60f1540 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -887,7 +887,7 @@ _f.Frm.prototype.set_intro = function(txt, append) { } _f.Frm.prototype.set_footnote = function(txt) { - frappe.utils.set_footnote(this, this.body, txt); + this.footnote_area = frappe.utils.set_footnote(this.footnote_area, this.body, txt); } diff --git a/frappe/public/js/lib/frappe-gantt/frappe-gantt.js b/frappe/public/js/lib/frappe-gantt/frappe-gantt.js index 4dab094142..c6d62278b4 100755 --- a/frappe/public/js/lib/frappe-gantt/frappe-gantt.js +++ b/frappe/public/js/lib/frappe-gantt/frappe-gantt.js @@ -86,6 +86,7 @@ return /******/ (function(modules) { // webpackBootstrap self.view_is = view_is; self.get_bar = get_bar; self.trigger_event = trigger_event; + self.refresh = refresh; // initialize with default view mode change_view_mode(self.config.view_mode); @@ -106,18 +107,29 @@ return /******/ (function(modules) { // webpackBootstrap }, padding: 18, view_mode: 'Day', - date_format: 'YYYY-MM-DD' + date_format: 'YYYY-MM-DD', + custom_popup_html: null }; + self.config = Object.assign({}, defaults, config); + + reset_variables(tasks); + } + + function reset_variables(tasks) { self.element = element; self._tasks = tasks; - self.config = Object.assign({}, defaults, config); self._bars = []; self._arrows = []; self.element_groups = {}; } + function refresh(updated_tasks) { + reset_variables(updated_tasks); + change_view_mode(self.config.view_mode); + } + function change_view_mode(mode) { set_scale(mode); prepare(); @@ -174,6 +186,12 @@ return /******/ (function(modules) { // webpackBootstrap } task.dependencies = deps; } + + // uids + if (!task.id) { + task.id = generate_id(task); + } + return task; }); } @@ -269,6 +287,7 @@ return /******/ (function(modules) { // webpackBootstrap } function prepare_canvas() { + if (self.canvas) return; self.canvas = Snap(self.element).addClass('gantt'); } @@ -373,7 +392,7 @@ return /******/ (function(modules) { // webpackBootstrap function set_width() { var cur_width = self.canvas.node.getBoundingClientRect().width; - var actual_width = self.canvas.getBBox().width; + var actual_width = self.canvas.select('#grid .grid-row').attr('width'); if (cur_width < actual_width) { self.canvas.attr('width', actual_width); } @@ -767,6 +786,10 @@ return /******/ (function(modules) { // webpackBootstrap }); } + function generate_id(task) { + return task.name + '_' + Math.random().toString(36).slice(2, 12); + } + function trigger_event(event, args) { if (self.config['on_' + event]) { self.config['on_' + event].apply(null, args); @@ -821,7 +844,7 @@ return /******/ (function(modules) { // webpackBootstrap // module - exports.push([module.id, ".gantt .grid-background {\n fill: none; }\n\n.gantt .grid-header {\n fill: #ffffff;\n stroke: #e0e0e0;\n stroke-width: 1.4; }\n\n.gantt .grid-row {\n fill: #ffffff; }\n\n.gantt .grid-row:nth-child(even) {\n fill: #f5f5f5; }\n\n.gantt .row-line {\n stroke: #ebeff2; }\n\n.gantt .tick {\n stroke: #e0e0e0;\n stroke-width: 0.2; }\n .gantt .tick.thick {\n stroke-width: 0.4; }\n\n.gantt .today-highlight {\n fill: #fcf8e3;\n opacity: 0.5; }\n\n.gantt #arrow {\n fill: none;\n stroke: #666;\n stroke-width: 1.4; }\n\n.gantt .bar {\n fill: #b8c2cc;\n stroke: #8D99A6;\n stroke-width: 0;\n transition: stroke-width .3s ease; }\n\n.gantt .bar-progress {\n fill: #a3a3ff; }\n\n.gantt .bar-invalid {\n fill: transparent;\n stroke: #8D99A6;\n stroke-width: 1;\n stroke-dasharray: 5; }\n .gantt .bar-invalid ~ .bar-label {\n fill: #555; }\n\n.gantt .bar-label {\n fill: #fff;\n dominant-baseline: central;\n text-anchor: middle;\n font-size: 12px;\n font-weight: lighter;\n letter-spacing: 0.8px; }\n .gantt .bar-label.big {\n fill: #555;\n text-anchor: start; }\n\n.gantt .handle {\n fill: #ddd;\n cursor: ew-resize;\n opacity: 0;\n visibility: hidden;\n transition: opacity .3s ease; }\n\n.gantt .bar-wrapper {\n cursor: pointer; }\n .gantt .bar-wrapper:hover .bar {\n stroke-width: 2; }\n .gantt .bar-wrapper:hover .handle {\n visibility: visible;\n opacity: 1; }\n .gantt .bar-wrapper.active .bar {\n stroke-width: 2; }\n\n.gantt .lower-text, .gantt .upper-text {\n font-size: 12px;\n text-anchor: middle; }\n\n.gantt .upper-text {\n fill: #555; }\n\n.gantt .lower-text {\n fill: #333; }\n\n.gantt #details {\n font-size: 14; }\n .gantt #details .details-container {\n stroke: #e0e0e0;\n stroke-width: 1.1;\n fill: #fff; }\n .gantt #details .details-heading {\n fill: #333;\n font-weight: 500; }\n .gantt #details .details-body {\n fill: #555; }\n\n.gantt .hide {\n display: none; }\n", "", {"version":3,"sources":["/./src/src/gantt.scss"],"names":[],"mappings":"AAYA;EAGE,WAAU,EACV;;AAJF;EAME,cAAa;EACb,gBAjBoB;EAkBpB,kBAAiB,EACjB;;AATF;EAWE,cAAa,EACb;;AAZF;EAcE,cAvBgB,EAwBhB;;AAfF;EAiBE,gBAzB0B,EA0B1B;;AAlBF;EAoBE,gBA9BoB;EA+BpB,kBAAiB,EAIjB;EAzBF;IAuBG,kBAAiB,EACjB;;AAxBH;EA2BE,cAlCoB;EAmCpB,aAAY,EACZ;;AA7BF;EAgCE,WAAU;EACV,aAvCe;EAwCf,kBAAiB,EACjB;;AAnCF;EAsCE,cAlDiB;EAmDjB,gBAlDkB;EAmDlB,gBAAe;EACf,kCAAiC,EACjC;;AA1CF;EA4CE,cA/CY,EAgDZ;;AA7CF;EA+CE,kBAAiB;EACjB,gBA3DkB;EA4DlB,gBAAe;EACf,oBAAmB,EAKnB;EAvDF;IAqDG,WA1Dc,EA2Dd;;AAtDH;EAyDE,WAAU;EACV,2BAA0B;EAC1B,oBAAmB;EACnB,gBAAe;EACf,qBAAoB;EACpB,sBAAqB,EAMrB;EApEF;IAiEG,WAtEc;IAuEd,mBAAkB,EAClB;;AAnEH;EAuEE,WAzEiB;EA0EjB,kBAAiB;EACjB,WAAU;EACV,mBAAkB;EAClB,6BAA4B,EAC5B;;AA5EF;EA+EE,gBAAe,EAkBf;EAjGF;IAmFI,gBAAe,EACf;EApFJ;IAuFI,oBAAmB;IACnB,WAAU,EACV;EAzFJ;IA8FI,gBAAe,EACf;;AA/FJ;EAoGE,gBAAe;EACf,oBAAmB,EACnB;;AAtGF;EAwGE,WA7Ge,EA8Gf;;AAzGF;EA2GE,WA/Ge,EAgHf;;AA5GF;EA+GE,cAAa,EAcb;EA7HF;IAkHG,gBA5HmB;IA6HnB,kBAAiB;IACjB,WAAU,EACV;EArHH;IAuHG,WA3Hc;IA4Hd,iBAAgB,EAChB;EAzHH;IA2HG,WAhIc,EAiId;;AA5HH;EAgIE,cAAa,EACb","file":"gantt.scss","sourcesContent":["$bar-color: #b8c2cc;\n$bar-stroke: #8D99A6;\n$border-color: #e0e0e0;\n$light-bg: #f5f5f5;\n$light-border-color: #ebeff2;\n$light-yellow: #fcf8e3;\n$text-muted: #666;\n$text-light: #555;\n$text-color: #333;\n$blue: #a3a3ff;\n$handle-color: #ddd;\n\n.gantt {\n\n\t.grid-background {\n\t\tfill: none;\n\t}\n\t.grid-header {\n\t\tfill: #ffffff;\n\t\tstroke: $border-color;\n\t\tstroke-width: 1.4;\n\t}\n\t.grid-row {\n\t\tfill: #ffffff;\n\t}\n\t.grid-row:nth-child(even) {\n\t\tfill: $light-bg;\n\t}\n\t.row-line {\n\t\tstroke: $light-border-color;\n\t}\n\t.tick {\n\t\tstroke: $border-color;\n\t\tstroke-width: 0.2;\n\t\t&.thick {\n\t\t\tstroke-width: 0.4;\n\t\t}\n\t}\n\t.today-highlight {\n\t\tfill: $light-yellow;\n\t\topacity: 0.5;\n\t}\n\n\t#arrow {\n\t\tfill: none;\n\t\tstroke: $text-muted;\n\t\tstroke-width: 1.4;\n\t}\n\n\t.bar {\n\t\tfill: $bar-color;\n\t\tstroke: $bar-stroke;\n\t\tstroke-width: 0;\n\t\ttransition: stroke-width .3s ease;\n\t}\n\t.bar-progress {\n\t\tfill: $blue;\n\t}\n\t.bar-invalid {\n\t\tfill: transparent;\n\t\tstroke: $bar-stroke;\n\t\tstroke-width: 1;\n\t\tstroke-dasharray: 5;\n\n\t\t&~.bar-label {\n\t\t\tfill: $text-light;\n\t\t}\n\t}\n\t.bar-label {\n\t\tfill: #fff;\n\t\tdominant-baseline: central;\n\t\ttext-anchor: middle;\n\t\tfont-size: 12px;\n\t\tfont-weight: lighter;\n\t\tletter-spacing: 0.8px;\n\n\t\t&.big {\n\t\t\tfill: $text-light;\n\t\t\ttext-anchor: start;\n\t\t}\n\t}\n\n\t.handle {\n\t\tfill: $handle-color;\n\t\tcursor: ew-resize;\n\t\topacity: 0;\n\t\tvisibility: hidden;\n\t\ttransition: opacity .3s ease;\n\t}\n\n\t.bar-wrapper {\n\t\tcursor: pointer;\n\n\t\t&:hover {\n\t\t\t.bar {\n\t\t\t\tstroke-width: 2;\n\t\t\t}\n\n\t\t\t.handle {\n\t\t\t\tvisibility: visible;\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\n\t\t&.active {\n\t\t\t.bar {\n\t\t\t\tstroke-width: 2;\n\t\t\t}\n\t\t}\n\t}\n\n\t.lower-text, .upper-text {\n\t\tfont-size: 12px;\n\t\ttext-anchor: middle;\n\t}\n\t.upper-text {\n\t\tfill: $text-light;\n\t}\n\t.lower-text {\n\t\tfill: $text-color;\n\t}\n\n\t#details {\n\t\tfont-size: 14;\n\n\t\t.details-container {\n\t\t\tstroke: $border-color;\n\t\t\tstroke-width: 1.1;\n\t\t\tfill: #fff;\n\t\t}\n\t\t.details-heading {\n\t\t\tfill: $text-color;\n\t\t\tfont-weight: 500;\n\t\t}\n\t\t.details-body {\n\t\t\tfill: $text-light;\n\t\t}\n\t}\n\n\t.hide {\n\t\tdisplay: none;\n\t}\n}"],"sourceRoot":"webpack://"}]); + exports.push([module.id, ".gantt .grid-background {\n fill: none; }\n\n.gantt .grid-header {\n fill: #ffffff;\n stroke: #e0e0e0;\n stroke-width: 1.4; }\n\n.gantt .grid-row {\n fill: #ffffff; }\n\n.gantt .grid-row:nth-child(even) {\n fill: #f5f5f5; }\n\n.gantt .row-line {\n stroke: #ebeff2; }\n\n.gantt .tick {\n stroke: #e0e0e0;\n stroke-width: 0.2; }\n .gantt .tick.thick {\n stroke-width: 0.4; }\n\n.gantt .today-highlight {\n fill: #fcf8e3;\n opacity: 0.5; }\n\n.gantt #arrow {\n fill: none;\n stroke: #666;\n stroke-width: 1.4; }\n\n.gantt .bar {\n fill: #b8c2cc;\n stroke: #8D99A6;\n stroke-width: 0;\n transition: stroke-width .3s ease; }\n\n.gantt .bar-progress {\n fill: #a3a3ff; }\n\n.gantt .bar-invalid {\n fill: transparent;\n stroke: #8D99A6;\n stroke-width: 1;\n stroke-dasharray: 5; }\n .gantt .bar-invalid ~ .bar-label {\n fill: #555; }\n\n.gantt .bar-label {\n fill: #fff;\n dominant-baseline: central;\n text-anchor: middle;\n font-size: 12px;\n font-weight: lighter;\n letter-spacing: 0.8px; }\n .gantt .bar-label.big {\n fill: #555;\n text-anchor: start; }\n\n.gantt .handle {\n fill: #ddd;\n cursor: ew-resize;\n opacity: 0;\n visibility: hidden;\n transition: opacity .3s ease; }\n\n.gantt .bar-wrapper {\n cursor: pointer; }\n .gantt .bar-wrapper:hover .bar {\n stroke-width: 2; }\n .gantt .bar-wrapper:hover .handle {\n visibility: visible;\n opacity: 1; }\n .gantt .bar-wrapper.active .bar {\n stroke-width: 2; }\n\n.gantt .lower-text, .gantt .upper-text {\n font-size: 12px;\n text-anchor: middle; }\n\n.gantt .upper-text {\n fill: #555; }\n\n.gantt .lower-text {\n fill: #333; }\n\n.gantt #details .details-container {\n background: #fff;\n display: inline-block;\n padding: 12px; }\n .gantt #details .details-container h5, .gantt #details .details-container p {\n margin: 0; }\n .gantt #details .details-container h5 {\n font-size: 12px;\n font-weight: bold;\n margin-bottom: 10px;\n color: #555; }\n .gantt #details .details-container p {\n font-size: 12px;\n margin-bottom: 6px;\n color: #666; }\n .gantt #details .details-container p:last-child {\n margin-bottom: 0; }\n\n.gantt .hide {\n display: none; }\n", "", {"version":3,"sources":["/./src/src/gantt.scss"],"names":[],"mappings":"AAYA;EAGE,WAAU,EACV;;AAJF;EAME,cAAa;EACb,gBAjBoB;EAkBpB,kBAAiB,EACjB;;AATF;EAWE,cAAa,EACb;;AAZF;EAcE,cAvBgB,EAwBhB;;AAfF;EAiBE,gBAzB0B,EA0B1B;;AAlBF;EAoBE,gBA9BoB;EA+BpB,kBAAiB,EAIjB;EAzBF;IAuBG,kBAAiB,EACjB;;AAxBH;EA2BE,cAlCoB;EAmCpB,aAAY,EACZ;;AA7BF;EAgCE,WAAU;EACV,aAvCe;EAwCf,kBAAiB,EACjB;;AAnCF;EAsCE,cAlDiB;EAmDjB,gBAlDkB;EAmDlB,gBAAe;EACf,kCAAiC,EACjC;;AA1CF;EA4CE,cA/CY,EAgDZ;;AA7CF;EA+CE,kBAAiB;EACjB,gBA3DkB;EA4DlB,gBAAe;EACf,oBAAmB,EAKnB;EAvDF;IAqDG,WA1Dc,EA2Dd;;AAtDH;EAyDE,WAAU;EACV,2BAA0B;EAC1B,oBAAmB;EACnB,gBAAe;EACf,qBAAoB;EACpB,sBAAqB,EAMrB;EApEF;IAiEG,WAtEc;IAuEd,mBAAkB,EAClB;;AAnEH;EAuEE,WAzEiB;EA0EjB,kBAAiB;EACjB,WAAU;EACV,mBAAkB;EAClB,6BAA4B,EAC5B;;AA5EF;EA+EE,gBAAe,EAkBf;EAjGF;IAmFI,gBAAe,EACf;EApFJ;IAuFI,oBAAmB;IACnB,WAAU,EACV;EAzFJ;IA8FI,gBAAe,EACf;;AA/FJ;EAoGE,gBAAe;EACf,oBAAmB,EACnB;;AAtGF;EAwGE,WA7Ge,EA8Gf;;AAzGF;EA2GE,WA/Ge,EAgHf;;AA5GF;EA+GE,iBAAgB;EAChB,sBAAqB;EACrB,cAAa,EAsBb;EAvIF;IAoHG,UAAS,EACT;EArHH;IAwHG,gBAAe;IACf,kBAAiB;IACjB,oBAAmB;IACnB,YAhIc,EAiId;EA5HH;IA+HG,gBAAe;IACf,mBAAkB;IAClB,YAvIc,EAwId;EAlIH;IAqIG,iBAAgB,EAChB;;AAtIH;EA0IE,cAAa,EACb","file":"gantt.scss","sourcesContent":["$bar-color: #b8c2cc;\n$bar-stroke: #8D99A6;\n$border-color: #e0e0e0;\n$light-bg: #f5f5f5;\n$light-border-color: #ebeff2;\n$light-yellow: #fcf8e3;\n$text-muted: #666;\n$text-light: #555;\n$text-color: #333;\n$blue: #a3a3ff;\n$handle-color: #ddd;\n\n.gantt {\n\n\t.grid-background {\n\t\tfill: none;\n\t}\n\t.grid-header {\n\t\tfill: #ffffff;\n\t\tstroke: $border-color;\n\t\tstroke-width: 1.4;\n\t}\n\t.grid-row {\n\t\tfill: #ffffff;\n\t}\n\t.grid-row:nth-child(even) {\n\t\tfill: $light-bg;\n\t}\n\t.row-line {\n\t\tstroke: $light-border-color;\n\t}\n\t.tick {\n\t\tstroke: $border-color;\n\t\tstroke-width: 0.2;\n\t\t&.thick {\n\t\t\tstroke-width: 0.4;\n\t\t}\n\t}\n\t.today-highlight {\n\t\tfill: $light-yellow;\n\t\topacity: 0.5;\n\t}\n\n\t#arrow {\n\t\tfill: none;\n\t\tstroke: $text-muted;\n\t\tstroke-width: 1.4;\n\t}\n\n\t.bar {\n\t\tfill: $bar-color;\n\t\tstroke: $bar-stroke;\n\t\tstroke-width: 0;\n\t\ttransition: stroke-width .3s ease;\n\t}\n\t.bar-progress {\n\t\tfill: $blue;\n\t}\n\t.bar-invalid {\n\t\tfill: transparent;\n\t\tstroke: $bar-stroke;\n\t\tstroke-width: 1;\n\t\tstroke-dasharray: 5;\n\n\t\t&~.bar-label {\n\t\t\tfill: $text-light;\n\t\t}\n\t}\n\t.bar-label {\n\t\tfill: #fff;\n\t\tdominant-baseline: central;\n\t\ttext-anchor: middle;\n\t\tfont-size: 12px;\n\t\tfont-weight: lighter;\n\t\tletter-spacing: 0.8px;\n\n\t\t&.big {\n\t\t\tfill: $text-light;\n\t\t\ttext-anchor: start;\n\t\t}\n\t}\n\n\t.handle {\n\t\tfill: $handle-color;\n\t\tcursor: ew-resize;\n\t\topacity: 0;\n\t\tvisibility: hidden;\n\t\ttransition: opacity .3s ease;\n\t}\n\n\t.bar-wrapper {\n\t\tcursor: pointer;\n\n\t\t&:hover {\n\t\t\t.bar {\n\t\t\t\tstroke-width: 2;\n\t\t\t}\n\n\t\t\t.handle {\n\t\t\t\tvisibility: visible;\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\n\t\t&.active {\n\t\t\t.bar {\n\t\t\t\tstroke-width: 2;\n\t\t\t}\n\t\t}\n\t}\n\n\t.lower-text, .upper-text {\n\t\tfont-size: 12px;\n\t\ttext-anchor: middle;\n\t}\n\t.upper-text {\n\t\tfill: $text-light;\n\t}\n\t.lower-text {\n\t\tfill: $text-color;\n\t}\n\n\t#details .details-container {\n\t\tbackground: #fff;\n\t\tdisplay: inline-block;\n\t\tpadding: 12px;\n\n\t\th5, p {\n\t\t\tmargin: 0;\n\t\t}\n\n\t\th5 {\n\t\t\tfont-size: 12px;\n\t\t\tfont-weight: bold;\n\t\t\tmargin-bottom: 10px;\n\t\t\tcolor: $text-light;\n\t\t}\n\n\t\tp {\n\t\t\tfont-size: 12px;\n\t\t\tmargin-bottom: 6px;\n\t\t\tcolor: $text-muted;\n\t\t}\n\n\t\tp:last-child {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\t}\n\n\t.hide {\n\t\tdisplay: none;\n\t}\n}"],"sourceRoot":"webpack://"}]); // exports @@ -1266,10 +1289,9 @@ return /******/ (function(modules) { // webpackBootstrap if (!self.details_box) { self.details_box = gt.canvas.group().addClass('details-wrapper hide').attr('data-task', self.task.id).appendTo(popover_group); - gt.canvas.rect(0, 0, 0, 110, 2, 2).addClass('details-container').appendTo(self.details_box); - gt.canvas.text(0, 0, '').attr({ dx: 10, dy: 30 }).addClass('details-heading').appendTo(self.details_box); - gt.canvas.text(0, 0, '').attr({ dx: 10, dy: 65 }).addClass('details-body').appendTo(self.details_box); - gt.canvas.text(0, 0, '').attr({ dx: 10, dy: 90 }).addClass('details-body').appendTo(self.details_box); + + render_details(); + var f = gt.canvas.filter(Snap.filter.shadow(0, 1, 1, '#666', 0.6)); self.details_box.attr({ filter: f @@ -1284,7 +1306,6 @@ return /******/ (function(modules) { // webpackBootstrap popover_group.selectAll('.details-wrapper').forEach(function (el) { return el.addClass('hide'); }); - render_details(); self.details_box.removeClass('hide'); }); } @@ -1295,23 +1316,35 @@ return /******/ (function(modules) { // webpackBootstrap y = _get_details_position.y; self.details_box.transform('t' + x + ',' + y); + self.details_box.clear(); + + var html = get_details_html(); + var foreign_object = Snap.parse('\n\t\t\t\t\n\t\t\t\t\t' + html + '\n\t\t\t\t\n\t\t\t\t'); + self.details_box.append(foreign_object); + } - var start_date = self.task._start.format('MMM D'), - end_date = self.task._end.format('MMM D'), - heading = self.task.name + ': ' + start_date + ' - ' + end_date; + function get_details_html() { - var $heading = self.details_box.select('.details-heading').attr('text', heading); + // custom html in config + if (gt.config.custom_popup_html) { + var _html = gt.config.custom_popup_html; + if (typeof _html === 'string') { + return _html; + } + if (isFunction(_html)) { + return _html(task); + } + } - var bbox = $heading.getBBox(); - self.details_box.select('.details-container').attr({ width: bbox.width + 20 }); + var start_date = self.task._start.format('MMM D'); + var end_date = self.task._end.format('MMM D'); + var heading = self.task.name + ': ' + start_date + ' - ' + end_date; - var duration = self.task._end.diff(self.task._start, 'days'), - body1 = 'Duration: ' + (duration + 1) + ' days', - body2 = self.task.progress ? 'Progress: ' + self.task.progress : ''; + var line_1 = 'Duration: ' + self.duration + ' days'; + var line_2 = self.task.progress ? 'Progress: ' + self.task.progress : null; - var $body = self.details_box.selectAll('.details-body'); - $body[0].attr('text', body1); - $body[1].attr('text', body2); + var html = '\n\t\t\t
    \n\t\t\t\t
    ' + heading + '
    \n\t\t\t\t

    ' + line_1 + '

    \n\t\t\t\t' + (line_2 ? '

    ' + line_2 + '

    ' : '') + '\n\t\t\t
    \n\t\t'; + return html; } function get_details_position() { @@ -1661,6 +1694,11 @@ return /******/ (function(modules) { // webpackBootstrap self.details_box && self.details_box.transform('t' + x + ',' + y); } + function isFunction(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + } + init(); return self; diff --git a/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css b/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css deleted file mode 100644 index 9bf18b91a5..0000000000 --- a/frappe/public/js/lib/gallery/css/blueimp-gallery-indicator.css +++ /dev/null @@ -1,72 +0,0 @@ -@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 deleted file mode 100644 index b47ea200b4..0000000000 --- a/frappe/public/js/lib/gallery/css/blueimp-gallery.css +++ /dev/null @@ -1,226 +0,0 @@ -@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 deleted file mode 100644 index 0470f6326a..0000000000 --- a/frappe/public/js/lib/gallery/js/blueimp-gallery-indicator.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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 deleted file mode 100644 index 0104f13728..0000000000 --- a/frappe/public/js/lib/gallery/js/blueimp-gallery.js +++ /dev/null @@ -1,1399 +0,0 @@ -/* - * 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) - } - }, - - nextSlides: function() { - // next set of images - this.next() - }, - - prevSlides: function() { - // prev set of images - this.prev() - }, - - 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) - this.nextSlides() - } - break - case 39: // Right - if (this.options.enableKeyboardNavigation) { - this.preventDefault(event) - this.next() - } - break - case 40: // Down - if (this.options.enableKeyboardNavigation) { - this.preventDefault(event) - this.prevSlides() - } - 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 -})) diff --git a/frappe/public/js/lib/photoswipe/default-skin.css b/frappe/public/js/lib/photoswipe/default-skin.css new file mode 100755 index 0000000000..f99db1be62 --- /dev/null +++ b/frappe/public/js/lib/photoswipe/default-skin.css @@ -0,0 +1,483 @@ +/*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license */ +/* + + Contents: + + 1. Buttons + 2. Share modal and links + 3. Index indicator ("1 of X" counter) + 4. Caption + 5. Loading indicator + 6. Additional styles (root element, top bar, idle state, hidden state, etc.) + +*/ +/* + + 1. Buttons + + */ +/*