From 05a51dd0f9feb9ded7249b3d4d27436420c2abb5 Mon Sep 17 00:00:00 2001 From: yisaka117 Date: Thu, 19 Jun 2025 09:27:49 +0900 Subject: [PATCH 01/11] #1166 Make it optional to require edit permission for parent data in order to edit child data --- resources/lang/en/exment.php | 2 ++ resources/lang/ja/exment.php | 2 ++ src/Controllers/CustomTableController.php | 3 +++ src/DataItems/Grid/DefaultGrid.php | 16 ++++++++++---- src/DataItems/Show/DefaultShow.php | 13 +++++++++--- src/Model/CustomValue.php | 26 +++++++++++++++++++---- 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/resources/lang/en/exment.php b/resources/lang/en/exment.php index b4e9a6dab..ba0676d25 100644 --- a/resources/lang/en/exment.php +++ b/resources/lang/en/exment.php @@ -1193,6 +1193,7 @@ 'all_user_viewable_flg' => 'All Users Can View', 'all_user_accessable_flg' => 'All Users Can Access', 'inherit_parent_permission' => 'Viewable with Permissions of Parent data', + 'editable_with_parent' => 'Need Edit Permission for Parend data', 'add_parent_menu_flg' => 'Add Menu', 'add_notify_flg' => 'Add To Notification', 'add_parent_menu' => 'Target Parent Menu', @@ -1273,6 +1274,7 @@ 'all_user_viewable_flg' => 'If set to YES, all users will be able to view all the data in this table.', 'all_user_accessable_flg' => 'If set to YES, all users will be able to see all the data in this table.
*It is not displayed on the menu or list page, it can be displayed only with internal data or reference from another table.', 'inherit_parent_permission' => 'If set to YES, the data can be viewed based on the the parent data(1:N) permissions. However, you must have viewing or similar permissions for this table itself.', + 'editable_with_parent' => 'If set to YES, you need edit permissions for the parent data(1:N) to edit the data in this table.', 'add_parent_menu_flg' => 'After creating custom table, you can add it to the menu. To add it, please set it to YES. *It will be displayed after updating the browser.
*It can be set only when new table is created. When updating please set it from "Menu" page.', 'add_parent_menu' => 'Please select the menu name to be parent.', 'add_notify_flg' => 'You can add settings for performing in-system notification to authorized users when creating/updating/sharing/commenting data, after creating a new table. Please add YES if you want to add.
* It can be set only when creating a new table. Please set from the "notification" page when updating.', diff --git a/resources/lang/ja/exment.php b/resources/lang/ja/exment.php index 64247a009..c763c5b8f 100644 --- a/resources/lang/ja/exment.php +++ b/resources/lang/ja/exment.php @@ -1194,6 +1194,7 @@ 'all_user_viewable_flg' => '全ユーザーが閲覧可能', 'all_user_accessable_flg' => '全ユーザーが参照可能', 'inherit_parent_permission' => '親データの権限で閲覧可能', + 'editable_with_parent' => '親データの編集権限が必要', 'add_parent_menu_flg' => 'メニューに追加する', 'add_notify_flg' => '通知に追加する', 'add_parent_menu' => '追加先の親メニュー', @@ -1274,6 +1275,7 @@ 'all_user_viewable_flg' => 'YESにした場合、すべてのユーザーが、このテーブルのすべてのデータを閲覧可能になります。', 'all_user_accessable_flg' => 'YESにした場合、すべてのユーザーが、このテーブルのすべてのデータを参照可能になります。
※メニューや一覧画面では表示されず、内部データや、他のテーブルからの参照でのみ表示できます。', 'inherit_parent_permission' => 'YESにした場合、親データ(1対多)の権限でデータを閲覧できます。ただし、このテーブル自体に担当者の閲覧等の権限を持っている必要があります。', + 'editable_with_parent' => 'YESにした場合、このテーブルのデータを編集するには親データ(1対多)の編集権限が必要になります。', 'add_parent_menu_flg' => '新規作成後、メニューに追加することができます。追加する場合はYESにしてください。
※ブラウザ更新後に表示されます。
※テーブルの新規作成時のみ設定できます。更新時は「メニュー」画面より設定してください。', 'add_parent_menu' => '親にするメニュー名を選択してください。', 'add_notify_flg' => 'データの新規作成・更新・共有、コメント時に、権限のあるユーザーに、システム内通知を行う設定を、テーブルの新規作成後に追加することができます。追加する場合はYESにしてください。
※テーブルの新規作成時のみ設定できます。更新時は「通知」画面より設定してください。', diff --git a/src/Controllers/CustomTableController.php b/src/Controllers/CustomTableController.php index 74a5c3044..34dca5d4b 100644 --- a/src/Controllers/CustomTableController.php +++ b/src/Controllers/CustomTableController.php @@ -318,6 +318,9 @@ protected function form($id = null) if ($has_parent) { $form->switchbool('inherit_parent_permission', exmtrans("custom_table.inherit_parent_permission"))->help(exmtrans("custom_table.help.inherit_parent_permission")) ->default("0"); + + $form->switchbool('editable_with_parent', exmtrans("custom_table.editable_with_parent"))->help(exmtrans("custom_table.help.editable_with_parent")) + ->default("1"); } })->disableHeader(); diff --git a/src/DataItems/Grid/DefaultGrid.php b/src/DataItems/Grid/DefaultGrid.php index 140f7584f..087ca1141 100644 --- a/src/DataItems/Grid/DefaultGrid.php +++ b/src/DataItems/Grid/DefaultGrid.php @@ -558,10 +558,18 @@ protected function manageRowAction($grid) $enableCreate = false; } - if (!is_null($parent_value = $actions->row->getParentValue(null, true)) && $parent_value->enableEdit(true) !== true) { - $enableCreate = false; - $enableEdit = false; - $enableDelete = false; + if (!is_null($parent_value = $actions->row->getParentValue(null, true))) { + if (boolval($custom_table->getOption('editable_with_parent')??1)) { + if ($parent_value->enableEdit(true) !== true) { + $enableCreate = false; + $enableEdit = false; + $enableDelete = false; + } + } elseif ($parent_value->enableAccess() !== true) { + $enableCreate = false; + $enableEdit = false; + $enableDelete = false; + } } if (!$enableEdit) { diff --git a/src/DataItems/Show/DefaultShow.php b/src/DataItems/Show/DefaultShow.php index ec99bb5fa..a3afdb806 100644 --- a/src/DataItems/Show/DefaultShow.php +++ b/src/DataItems/Show/DefaultShow.php @@ -170,9 +170,16 @@ public function createShowForm() $tools->disableList(); } - if (!is_null($parent_value = $this->custom_value->getParentValue(null, true)) && $parent_value->enableEdit(true) !== true) { - $tools->disableEdit(); - $tools->disableDelete(); + if (!is_null($parent_value = $this->custom_value->getParentValue(null, true))) { + if (boolval($this->custom_table->getOption('editable_with_parent')??1)) { + if ($parent_value->enableEdit(true) !== true) { + $tools->disableEdit(); + $tools->disableDelete(); + } + } elseif ($parent_value->enableAccess() !== true) { + $tools->disableEdit(); + $tools->disableDelete(); + } } if ($this->modal) { diff --git a/src/Model/CustomValue.php b/src/Model/CustomValue.php index 8b6718241..27c02193b 100644 --- a/src/Model/CustomValue.php +++ b/src/Model/CustomValue.php @@ -1794,8 +1794,17 @@ public function enableEdit($checkFormAction = false) return ErrorCode::WORKFLOW_LOCK(); } - if (!is_null($parent_value = $this->getParentValue()) && ($code = $parent_value->enableEdit($checkFormAction)) !== true) { - return $code; + // check parent permission + if (!is_null($parent_value = $this->getParentValue())) { + if (boolval($this->custom_table->getOption('editable_with_parent')??1)) { + if (($code = $parent_value->enableEdit($checkFormAction)) !== true) { + return $code; + } + } else { + if (($code = $parent_value->enableAccess()) !== true) { + return $code; + } + } } if ($this->trashed()) { @@ -1834,8 +1843,17 @@ public function enableDelete($checkFormAction = false) return ErrorCode::DELETE_DISABLED(); } - if (!is_null($parent_value = $this->getParentValue()) && ($code = $parent_value->enableDelete($checkFormAction)) !== true) { - return $code; + // check parent permission + if (!is_null($parent_value = $this->getParentValue())) { + if (boolval($this->custom_table->getOption('editable_with_parent')??1)) { + if (($code = $parent_value->enableDelete($checkFormAction)) !== true) { + return $code; + } + } else { + if (($code = $parent_value->enableAccess()) !== true) { + return $code; + } + } } return true; From 715712a82d8f25fe83c3d685a5af36962cbca687 Mon Sep 17 00:00:00 2001 From: ex1nhatvh+manhdd Date: Thu, 24 Jul 2025 17:02:00 +0700 Subject: [PATCH 02/11] #1627 Fix sort in summary view --- src/Services/Search/SearchService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php index 26dc4ada6..ac9bfc676 100644 --- a/src/Services/Search/SearchService.php +++ b/src/Services/Search/SearchService.php @@ -466,7 +466,7 @@ protected function setSummaryOrderBy($column, $wrap_column) { $sort_order = array_get($column->options, 'sort_order'); if (is_nullorempty($sort_order)) { - return $this; + $sort_order = 1; } $sort_type = isMatchString(array_get($column->options, 'sort_type'), '-1') ? 'desc' : 'asc'; From b7964a021dab7ac50e5e89751b15526cf063c6a2 Mon Sep 17 00:00:00 2001 From: yisaka117 Date: Wed, 13 Aug 2025 17:24:18 +0900 Subject: [PATCH 03/11] add parents option to get custom_value api --- src/Controllers/ApiDataTrait.php | 61 +++++++++++++ tests/Feature/Api2Test.php | 147 +++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/src/Controllers/ApiDataTrait.php b/src/Controllers/ApiDataTrait.php index 76aa611ee..dbdf0f140 100644 --- a/src/Controllers/ApiDataTrait.php +++ b/src/Controllers/ApiDataTrait.php @@ -16,6 +16,7 @@ use Exceedone\Exment\Enums\SearchType; use Exceedone\Exment\Enums\ValueType; use Exceedone\Exment\Enums\ErrorCode; +use Exceedone\Exment\Enums\RelationType; use Validator; /** @@ -202,6 +203,7 @@ protected function modifyAfterGetValue(Request $request, $target, $options = []) 'id', 'target_view_id', 'children', + 'parents', ]), $options['appends'] ); @@ -267,6 +269,11 @@ protected function modifyCustomValue(Request $request, $custom_value, $recursive $custom_value = $this->modifyChildrenValue($request, $custom_value); } + // Change relation key name + if (!$recursive && $request->has('parents') && boolval($request->get('parents'))) { + $custom_value = $this->modifyParentsValue($request, $custom_value); + } + // convert to custom values $valuetype = $request->get('valuetype'); if ($request->has('valuetype') && ValueType::isRegetApiCustomValue($valuetype)) { @@ -386,10 +393,22 @@ protected function setQueryInfo($query) $query->with($relation->getRelationName()); } } + if ($request->has('parents') && boolval($request->get('parents'))) { + $relations = CustomRelation::getRelationsByChild($this->custom_table); + foreach ($relations as $relation) { + $query->with($relation->getRelationName()); + } + } return $query; } + /** + * Modify children values (exclude virtual columns from children values) + * + * @param Request $request + * @return CustomValue $custom_value + */ protected function modifyChildrenValue(Request $request, $custom_value) { $relations = CustomRelation::getRelationsByParent($this->custom_table); @@ -417,4 +436,46 @@ protected function modifyChildrenValue(Request $request, $custom_value) $custom_value['children'] = $results; return $custom_value; } + + /** + * Modify parents values (exclude virtual columns from parents values) + * + * @param Request $request + * @return CustomValue $custom_value + */ + protected function modifyParentsValue(Request $request, $custom_value) + { + $relations = CustomRelation::getRelationsByChild($this->custom_table); + + $results = []; + foreach ($relations as $relation) { + // If getted relation name, change key name + $reltionName = $relation->getRelationName(); + if (array_has($custom_value, $reltionName)) { + $relationValues = $custom_value[$reltionName]; + $makeHiddenArray = $relation->parent_custom_table_cache->getMakeHiddenArray(); + + if ($relation->relation_type == RelationType::ONE_TO_MANY) { + // Call makehidden + $relationValues = $relationValues->makeHidden($makeHiddenArray); + // Call modify custom value + $relationValues = $this->modifyCustomValue($request, $relationValues, true); + } else { + $relationValues = $relationValues->map(function ($relationValue) use ($makeHiddenArray, $request) { + // Call makehidden + $relationValue = $relationValue->makeHidden($makeHiddenArray); + // Call modify custom value + $relationValue = $this->modifyCustomValue($request, $relationValue, true); + return $relationValue; + }); + } + // Set key name + $results[$relation->parent_custom_table_cache->table_name] = $relationValues; + unset($custom_value[$reltionName]); + } + } + + $custom_value['parents'] = $results; + return $custom_value; + } } diff --git a/tests/Feature/Api2Test.php b/tests/Feature/Api2Test.php index 2618fff15..cfc4315d4 100644 --- a/tests/Feature/Api2Test.php +++ b/tests/Feature/Api2Test.php @@ -662,6 +662,56 @@ public function testGetValuesWithChildren2() } } + /** + * Getting values parents(1:N), and match ids + */ + /** + * @return void + */ + public function testGetValuesWithParents() + { + $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]); + + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE).'?parents=1&count=5') + ->assertStatus(200) + ->assertJsonCount(5, 'data'); + + // check children + $json = json_decode_ex($response->baseResponse->getContent(), true); + $data = array_get($json, 'data'); + + foreach ($data as $d) { + $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, array_get($d, 'id')); + } + } + + /** + * Getting values parents(N:N), and match ids + */ + /** + * @return void + */ + public function testGetValuesWithParents2() + { + $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]); + + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY).'?parents=1&count=5&page=2') + ->assertStatus(200) + ->assertJsonCount(5, 'data'); + + // check children + $json = json_decode_ex($response->baseResponse->getContent(), true); + $data = array_get($json, 'data'); + + foreach ($data as $d) { + $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, array_get($d, 'id')); + } + } + /** * @return void */ @@ -808,6 +858,52 @@ public function testGetValueWithChildren2() $this->_testChildrenValues($d, TestDefine::TESTDATA_TABLE_NAME_PARENT_TABLE_MANY_TO_MANY, array_get($d, 'id')); } + /** + * Getting value with parents, and match ids + */ + /** + * @return void + */ + public function testGetValueWithParents() + { + $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]); + + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, '1?parents=1')) + ->assertStatus(200) + ->assertJsonFragment([ + 'id' => 1 + ]); + + // check children + $d = json_decode_ex($response->baseResponse->getContent(), true); + $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, array_get($d, 'id')); + } + + /** + * Getting value with parents, and match ids + */ + /** + * @return void + */ + public function testGetValueWithParents2() + { + $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]); + + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, '2?parents=1')) + ->assertStatus(200) + ->assertJsonFragment([ + 'id' => 2 + ]); + + // check children + $d = json_decode_ex($response->baseResponse->getContent(), true); + $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, array_get($d, 'id')); + } + /** * @return void */ @@ -3038,4 +3134,55 @@ protected function _testChildrenValues($data, $table_name, $id) } } } + + /** + * @param mixed $data + * @param string $table_name + * @param mixed $id + * @return void + */ + protected function _testParentsValues($data, $table_name, $id) + { + $relations = CustomRelation::getRelationsByChild($table_name); + + // Whether has children + $this->assertTrue(array_has($data, 'parents')); + foreach ($relations as $relation) { + $this->assertTrue(array_has($data, 'parents.' . $relation->parent_custom_table_cache->table_name)); + $parents = array_get($data, 'parents.' . $relation->parent_custom_table_cache->table_name); + + if ($relation->relation_type == RelationType::ONE_TO_MANY) { + // Get value directly with parent_id + $value = $relation->child_custom_table_cache->getValueQuery()->find($id); + $parentId = array_get($parents, 'id'); + $this->assertTrue($parentId == $value->parent_id, "parent_id expects equals {$value->parent_id}', but {$parentId}."); + } + ////// Check as n:n + else { + // get parents id + $parents_ids = collect($parents)->map(function ($parent) { + return array_get($parent, 'id'); + })->toArray(); + + // Get value using pivot table + $parentIds = \DB::table($relation->getRelationName()) + ->where('child_id', $id) + ->select('parent_id') + ->distinct() + ->pluck('parent_id') + ->toArray(); + + if (count($parentIds) == 0) { + $this->assertTrue(count($parents_ids) == 0, "{$id}' parents count expects 0, but real count is " . count($parents_ids)); + } else { + foreach ($parentIds as $parentId) { + $this->assertTrue(in_array($parentId, $parents_ids), "{$parentId} expects containing {$id}' parents, but not has."); + } + foreach ($parents_ids as $parentId) { + $this->assertTrue(in_array($parentId, $parentIds), "{$parentId} expects containing {$id}' parents, but not has."); + } + } + } + } + } } From 8d44b176d94d6ea26cc98118086747e5866d8a37 Mon Sep 17 00:00:00 2001 From: yisaka117 Date: Thu, 11 Sep 2025 17:46:39 +0900 Subject: [PATCH 04/11] add a consistency check when executing workflow actions --- resources/lang/en/exment.php | 1 + resources/lang/ja/exment.php | 1 + src/Controllers/CustomValueController.php | 11 ++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/lang/en/exment.php b/resources/lang/en/exment.php index 205fc42fc..cca5cdd3d 100644 --- a/resources/lang/en/exment.php +++ b/resources/lang/en/exment.php @@ -1150,6 +1150,7 @@ 'get_by_userinfo_and_action_select' => 'In the same pre-execution status, "Get from execution user information" and "Select by execution user of previous action" cannot be set at the same time.', 'action_execute' => 'Perform the following actions:', 'nextuser_not_found' => 'The following working user does not exist. Please contact the administrator.', + 'status_changed' => 'This action cannot be performed. Another user may have executed the workflow.', ], 'comment_options' => [ diff --git a/resources/lang/ja/exment.php b/resources/lang/ja/exment.php index 07cc6ffe6..3f76af86c 100644 --- a/resources/lang/ja/exment.php +++ b/resources/lang/ja/exment.php @@ -1151,6 +1151,7 @@ 'ignore_work_and_action_select' => '実行可能ユーザーが「前アクションの実行ユーザーが選択」の場合、「特殊なアクション」を設定できません。', 'action_execute' => '以下のアクションを実行します。', 'nextuser_not_found' => '次の作業ユーザーが存在しません。管理者に問い合わせください。', + 'status_changed' => 'このアクションは実行できません。他のユーザーがワークフローを実行した可能性があります。', ], 'comment_options' => [ diff --git a/src/Controllers/CustomValueController.php b/src/Controllers/CustomValueController.php index 8a25edac1..5f36b0823 100644 --- a/src/Controllers/CustomValueController.php +++ b/src/Controllers/CustomValueController.php @@ -717,7 +717,16 @@ public function actionClick(Request $request, $tableKey, $id) $custom_value = $this->custom_table->getValueModel($id); - //TODO:validation + //validation + $workflow_actions = $custom_value->getWorkflowActions(true); + if (!$workflow_actions->contains(function($workflow_action) use($action){ + return $workflow_action->id == $action->id; + })) { + return ([ + 'result' => false, + 'toastr' => sprintf(exmtrans('workflow.message.status_changed')), + ]); + } $action->executeAction($custom_value, [ 'comment' => $request->get('comment'), From 47df561d8fed33018f576dc8b5a44e28f623cd24 Mon Sep 17 00:00:00 2001 From: yisaka117 Date: Thu, 11 Sep 2025 17:49:41 +0900 Subject: [PATCH 05/11] Revert "add a consistency check when executing workflow actions" This reverts commit 8d44b176d94d6ea26cc98118086747e5866d8a37. --- resources/lang/en/exment.php | 1 - resources/lang/ja/exment.php | 1 - src/Controllers/CustomValueController.php | 11 +---------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/resources/lang/en/exment.php b/resources/lang/en/exment.php index cca5cdd3d..205fc42fc 100644 --- a/resources/lang/en/exment.php +++ b/resources/lang/en/exment.php @@ -1150,7 +1150,6 @@ 'get_by_userinfo_and_action_select' => 'In the same pre-execution status, "Get from execution user information" and "Select by execution user of previous action" cannot be set at the same time.', 'action_execute' => 'Perform the following actions:', 'nextuser_not_found' => 'The following working user does not exist. Please contact the administrator.', - 'status_changed' => 'This action cannot be performed. Another user may have executed the workflow.', ], 'comment_options' => [ diff --git a/resources/lang/ja/exment.php b/resources/lang/ja/exment.php index 3f76af86c..07cc6ffe6 100644 --- a/resources/lang/ja/exment.php +++ b/resources/lang/ja/exment.php @@ -1151,7 +1151,6 @@ 'ignore_work_and_action_select' => '実行可能ユーザーが「前アクションの実行ユーザーが選択」の場合、「特殊なアクション」を設定できません。', 'action_execute' => '以下のアクションを実行します。', 'nextuser_not_found' => '次の作業ユーザーが存在しません。管理者に問い合わせください。', - 'status_changed' => 'このアクションは実行できません。他のユーザーがワークフローを実行した可能性があります。', ], 'comment_options' => [ diff --git a/src/Controllers/CustomValueController.php b/src/Controllers/CustomValueController.php index 5f36b0823..8a25edac1 100644 --- a/src/Controllers/CustomValueController.php +++ b/src/Controllers/CustomValueController.php @@ -717,16 +717,7 @@ public function actionClick(Request $request, $tableKey, $id) $custom_value = $this->custom_table->getValueModel($id); - //validation - $workflow_actions = $custom_value->getWorkflowActions(true); - if (!$workflow_actions->contains(function($workflow_action) use($action){ - return $workflow_action->id == $action->id; - })) { - return ([ - 'result' => false, - 'toastr' => sprintf(exmtrans('workflow.message.status_changed')), - ]); - } + //TODO:validation $action->executeAction($custom_value, [ 'comment' => $request->get('comment'), From 3918b05e676187db3ca5f908292e22ba1928bdc6 Mon Sep 17 00:00:00 2001 From: SuDC Date: Wed, 10 Dec 2025 10:47:54 +0700 Subject: [PATCH 06/11] Add scroll position fix script --- .../vendor/exment/js/scroll-position-fix.js | 195 ++++++++++++++++++ src/Middleware/Bootstrap.php | 1 + 2 files changed, 196 insertions(+) create mode 100644 public/vendor/exment/js/scroll-position-fix.js diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js new file mode 100644 index 000000000..02c289a2c --- /dev/null +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -0,0 +1,195 @@ +/** + * Fix scroll position restore issue after browser back button + * For custom table list view in Exment + */ +(function() { + 'use strict'; + + // Storage key for scroll positions + const SCROLL_POSITION_KEY = 'exment_scroll_positions'; + const MAX_HISTORY = 50; // Keep last 50 positions + const CLASSNAME_CUSTOM_VALUE_GRID = 'block_custom_value_grid'; + + // Get current page identifier + function getPageKey() { + return window.location.pathname + window.location.search; + } + + // Get all scroll positions from sessionStorage + function getScrollPositions() { + try { + const data = sessionStorage.getItem(SCROLL_POSITION_KEY); + return data ? JSON.parse(data) : {}; + } catch (e) { + console.warn('Failed to load scroll positions:', e); + return {}; + } + } + + // Save scroll positions to sessionStorage + function saveScrollPositions(positions) { + try { + // Limit the number of stored positions + const keys = Object.keys(positions); + if (keys.length > MAX_HISTORY) { + // Remove oldest entries + keys.slice(0, keys.length - MAX_HISTORY).forEach(key => { + delete positions[key]; + }); + } + sessionStorage.setItem(SCROLL_POSITION_KEY, JSON.stringify(positions)); + } catch (e) { + console.warn('Failed to save scroll positions:', e); + } + } + + // Save current scroll position + function saveCurrentScrollPosition() { + const pageKey = getPageKey(); + const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; + + const positions = getScrollPositions(); + positions[pageKey] = { + scrollTop: scrollTop, + timestamp: Date.now() + }; + saveScrollPositions(positions); + + // Also store in history state if available + if (window.history && window.history.replaceState) { + const currentState = window.history.state || {}; + currentState.scrollTop = scrollTop; + try { + window.history.replaceState(currentState, document.title); + } catch (e) { + console.warn('Failed to update history state:', e); + } + } + } + + // Restore scroll position + function restoreScrollPosition() { + const pageKey = getPageKey(); + const positions = getScrollPositions(); + const savedPosition = positions[pageKey]; + + let scrollTop = 0; + + // Try to get from history state first + if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { + scrollTop = window.history.state.scrollTop; + } + // Fallback to sessionStorage + else if (savedPosition && typeof savedPosition.scrollTop === 'number') { + scrollTop = savedPosition.scrollTop; + } + + if (scrollTop > 0) { + // Use setTimeout to ensure DOM is ready + setTimeout(function() { + $('html, body').scrollTop(scrollTop); + $(window).scrollTop(scrollTop); + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; + }, 0); + + // Fallback restore after a bit longer delay + setTimeout(function() { + if (Math.abs($(window).scrollTop() - scrollTop) > 50) { + $('html, body').scrollTop(scrollTop); + } + }, 100); + } + } + + // Clear old scroll positions (older than 1 hour) + function clearOldPositions() { + const positions = getScrollPositions(); + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + + Object.keys(positions).forEach(key => { + if (now - positions[key].timestamp > oneHour) { + delete positions[key]; + } + }); + + saveScrollPositions(positions); + } + + // Initialize on document ready + $(function() { + // Disable browser's default scroll restoration + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + + // Clear old positions on page load + clearOldPositions(); + + // Restore scroll position on page load + restoreScrollPosition(); + + // Save scroll position periodically while scrolling + let scrollTimer; + $(window).on('scroll', function() { + clearTimeout(scrollTimer); + scrollTimer = setTimeout(function() { + // Only save if we're on a list page (grid view) + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }, 150); + }); + + // Save scroll position before leaving page + $(window).on('beforeunload', function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Handle pjax events + if ($.pjax) { + // Save scroll position before pjax request + $(document).on('pjax:send', function(event, xhr, options) { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Restore scroll position after pjax complete + $(document).on('pjax:complete', function(event, xhr, textStatus, options) { + // Wait for content to be rendered + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 50); + }); + + // Also try to restore on pjax:end + $(document).on('pjax:end', function(event, xhr, options) { + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 100); + }); + } + + // Handle browser back/forward buttons + $(window).on('popstate', function(event) { + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 50); + }); + + // Save position when clicking on links in grid + $('.' + CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { + saveCurrentScrollPosition(); + }); + }); +})(); diff --git a/src/Middleware/Bootstrap.php b/src/Middleware/Bootstrap.php index 4c57caa8d..507ba13d0 100644 --- a/src/Middleware/Bootstrap.php +++ b/src/Middleware/Bootstrap.php @@ -77,6 +77,7 @@ protected function setCssJs(Request $request, \Closure $next) 'vendor/exment/jstree/jstree.min.js', 'vendor/exment/js/common_all.js', 'vendor/exment/js/common.js', + 'vendor/exment/js/scroll-position-fix.js', 'vendor/exment/js/search.js', 'vendor/exment/js/calc.js', 'vendor/exment/js/notify_navbar.js', From 6987e85e17c21b19e1be9df7050613ae6cb1b9b6 Mon Sep 17 00:00:00 2001 From: ex1anhth+quangdv Date: Wed, 10 Dec 2025 13:31:44 +0900 Subject: [PATCH 07/11] Prevent null error in MailSendJob by passing user ID instead of object --- src/Jobs/MailSendJob.php | 8 ++++---- src/Notifications/MailSender.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Jobs/MailSendJob.php b/src/Jobs/MailSendJob.php index feabdeea3..1d9dc0d33 100644 --- a/src/Jobs/MailSendJob.php +++ b/src/Jobs/MailSendJob.php @@ -28,13 +28,13 @@ class MailSendJob extends Notification implements ShouldQueue * @var MailHistory */ protected $mailHistory; - protected $user; + protected $userId; protected $finalUser; - public function __construct($user = null, $finalUser = false) + public function __construct($userId = null, $finalUser = false) { - $this->user = $user; + $this->userId = $userId; $this->finalUser = $finalUser; } @@ -104,7 +104,7 @@ public function failed($exception) $mail_template->getValue('mail_subject'), $mail_template->getValue('mail_body'), $this->notify_id ?? -1, - $this->user->id, + $this->userId, \Exment::getUserId() ?? null, $this->mailHistory->getParentId(), $this->mailHistory->getParentType() diff --git a/src/Notifications/MailSender.php b/src/Notifications/MailSender.php index 2acdc66e7..2973c3dc8 100644 --- a/src/Notifications/MailSender.php +++ b/src/Notifications/MailSender.php @@ -241,7 +241,7 @@ protected function sendMail() ->setFromName($fromName) ->setBodyType($bodyType); - $job = new MailSendJob(\Exment::user(), $this->final_user); + $job = new MailSendJob(\Exment::getUserId(), $this->final_user); $job->setMailInfo($this->mailInfo) ->setMailHistory($this->mailHistory); $this->notify($job); @@ -281,7 +281,7 @@ protected function sendPasswordMail() ->setMailTemplate($mail_template) ->setHistory(false); - $job = new MailSendJob(\Exment::user(), $this->final_user); + $job = new MailSendJob(\Exment::getUserId(), $this->final_user); $job->setMailInfo($mailInfo) ->setMailHistory($mailHistory); From 29edfc1ad5f9e07e572c313baa74b857aceb92f0 Mon Sep 17 00:00:00 2001 From: SuDC Date: Mon, 15 Dec 2025 15:09:06 +0700 Subject: [PATCH 08/11] Sort and limit stored scroll positions by timestamp --- public/vendor/exment/js/scroll-position-fix.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 02c289a2c..06f49c0ed 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -32,8 +32,11 @@ // Limit the number of stored positions const keys = Object.keys(positions); if (keys.length > MAX_HISTORY) { - // Remove oldest entries - keys.slice(0, keys.length - MAX_HISTORY).forEach(key => { + // Sort by timestamp and remove oldest entries + const sortedKeys = keys.sort((a, b) => { + return (positions[a].timestamp || 0) - (positions[b].timestamp || 0); + }); + sortedKeys.slice(0, keys.length - MAX_HISTORY).forEach(key => { delete positions[key]; }); } From f3152f78c6434b88c6466d9c501eb34d748b5556 Mon Sep 17 00:00:00 2001 From: SuDC Date: Mon, 15 Dec 2025 16:08:40 +0700 Subject: [PATCH 09/11] Enhance scroll position restoration and validation logic --- .../vendor/exment/js/scroll-position-fix.js | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 06f49c0ed..8d2bb8b7d 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -90,18 +90,15 @@ if (scrollTop > 0) { // Use setTimeout to ensure DOM is ready setTimeout(function() { - $('html, body').scrollTop(scrollTop); - $(window).scrollTop(scrollTop); - document.documentElement.scrollTop = scrollTop; - document.body.scrollTop = scrollTop; + window.scrollTo(0, scrollTop); + // Verify and retry if needed + setTimeout(function() { + const currentScroll = window.pageYOffset || document.documentElement.scrollTop; + if (Math.abs(currentScroll - scrollTop) > 50) { + window.scrollTo(0, scrollTop); + } + }, 100); }, 0); - - // Fallback restore after a bit longer delay - setTimeout(function() { - if (Math.abs($(window).scrollTop() - scrollTop) > 50) { - $('html, body').scrollTop(scrollTop); - } - }, 100); } } @@ -112,7 +109,13 @@ const oneHour = 60 * 60 * 1000; Object.keys(positions).forEach(key => { - if (now - positions[key].timestamp > oneHour) { + try { + const position = positions[key]; + if (!position || typeof position !== 'object' || !position.timestamp || now - position.timestamp > oneHour) { + delete positions[key]; + } + } catch (e) { + // Remove invalid entry delete positions[key]; } }); @@ -122,6 +125,12 @@ // Initialize on document ready $(function() { + // Prevent multiple initialization + if (window.exmentScrollFixInitialized) { + return; + } + window.exmentScrollFixInitialized = true; + // Disable browser's default scroll restoration if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; From cedab8a700abcffc43e3a759854b57473224ef3c Mon Sep 17 00:00:00 2001 From: SuDC Date: Tue, 16 Dec 2025 14:55:20 +0700 Subject: [PATCH 10/11] Refactor scroll position handling --- .../vendor/exment/js/scroll-position-fix.js | 126 +++--------------- 1 file changed, 19 insertions(+), 107 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 8d2bb8b7d..1aa509387 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -5,60 +5,13 @@ (function() { 'use strict'; - // Storage key for scroll positions - const SCROLL_POSITION_KEY = 'exment_scroll_positions'; - const MAX_HISTORY = 50; // Keep last 50 positions const CLASSNAME_CUSTOM_VALUE_GRID = 'block_custom_value_grid'; - // Get current page identifier - function getPageKey() { - return window.location.pathname + window.location.search; - } - - // Get all scroll positions from sessionStorage - function getScrollPositions() { - try { - const data = sessionStorage.getItem(SCROLL_POSITION_KEY); - return data ? JSON.parse(data) : {}; - } catch (e) { - console.warn('Failed to load scroll positions:', e); - return {}; - } - } - - // Save scroll positions to sessionStorage - function saveScrollPositions(positions) { - try { - // Limit the number of stored positions - const keys = Object.keys(positions); - if (keys.length > MAX_HISTORY) { - // Sort by timestamp and remove oldest entries - const sortedKeys = keys.sort((a, b) => { - return (positions[a].timestamp || 0) - (positions[b].timestamp || 0); - }); - sortedKeys.slice(0, keys.length - MAX_HISTORY).forEach(key => { - delete positions[key]; - }); - } - sessionStorage.setItem(SCROLL_POSITION_KEY, JSON.stringify(positions)); - } catch (e) { - console.warn('Failed to save scroll positions:', e); - } - } - // Save current scroll position function saveCurrentScrollPosition() { - const pageKey = getPageKey(); const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; - const positions = getScrollPositions(); - positions[pageKey] = { - scrollTop: scrollTop, - timestamp: Date.now() - }; - saveScrollPositions(positions); - - // Also store in history state if available + // Store in history state if available if (window.history && window.history.replaceState) { const currentState = window.history.state || {}; currentState.scrollTop = scrollTop; @@ -70,21 +23,13 @@ } } - // Restore scroll position + // Restore scroll position from history state function restoreScrollPosition() { - const pageKey = getPageKey(); - const positions = getScrollPositions(); - const savedPosition = positions[pageKey]; - let scrollTop = 0; - // Try to get from history state first + // Only restore from history state (for back button) if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { scrollTop = window.history.state.scrollTop; - } - // Fallback to sessionStorage - else if (savedPosition && typeof savedPosition.scrollTop === 'number') { - scrollTop = savedPosition.scrollTop; } if (scrollTop > 0) { @@ -102,27 +47,6 @@ } } - // Clear old scroll positions (older than 1 hour) - function clearOldPositions() { - const positions = getScrollPositions(); - const now = Date.now(); - const oneHour = 60 * 60 * 1000; - - Object.keys(positions).forEach(key => { - try { - const position = positions[key]; - if (!position || typeof position !== 'object' || !position.timestamp || now - position.timestamp > oneHour) { - delete positions[key]; - } - } catch (e) { - // Remove invalid entry - delete positions[key]; - } - }); - - saveScrollPositions(positions); - } - // Initialize on document ready $(function() { // Prevent multiple initialization @@ -136,12 +60,8 @@ history.scrollRestoration = 'manual'; } - // Clear old positions on page load - clearOldPositions(); - - // Restore scroll position on page load restoreScrollPosition(); - + // Save scroll position periodically while scrolling let scrollTimer; $(window).on('scroll', function() { @@ -163,6 +83,12 @@ // Handle pjax events if ($.pjax) { + let isPjaxPopstate = false; + + $(document).on('pjax:popstate', function() { + isPjaxPopstate = true; + }); + // Save scroll position before pjax request $(document).on('pjax:send', function(event, xhr, options) { if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { @@ -170,35 +96,21 @@ } }); - // Restore scroll position after pjax complete - $(document).on('pjax:complete', function(event, xhr, textStatus, options) { - // Wait for content to be rendered - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - restoreScrollPosition(); - } - }, 50); - }); - - // Also try to restore on pjax:end + // Handle scroll position after pjax complete $(document).on('pjax:end', function(event, xhr, options) { - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + if (isPjaxPopstate) { + // If it's a back/forward navigation, restore scroll restoreScrollPosition(); + isPjaxPopstate = false; + } else { + // If it's a new navigation (pagination, sort, etc), scroll to top + window.scrollTo(0, 0); } - }, 100); + } }); } - // Handle browser back/forward buttons - $(window).on('popstate', function(event) { - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - restoreScrollPosition(); - } - }, 50); - }); - // Save position when clicking on links in grid $('.' + CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { saveCurrentScrollPosition(); From 8987e49958b9db3ed266f3b3da6e60a7d2fe01c4 Mon Sep 17 00:00:00 2001 From: vohoangnhat Date: Tue, 20 Jan 2026 14:25:14 +0700 Subject: [PATCH 11/11] Scroll restoration handling --- .../vendor/exment/js/scroll-position-fix.js | 119 ---------------- public/vendor/exment/js/scroll-restore.js | 128 ++++++++++++++++++ src/Middleware/Bootstrap.php | 2 +- 3 files changed, 129 insertions(+), 120 deletions(-) delete mode 100644 public/vendor/exment/js/scroll-position-fix.js create mode 100644 public/vendor/exment/js/scroll-restore.js diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js deleted file mode 100644 index 1aa509387..000000000 --- a/public/vendor/exment/js/scroll-position-fix.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Fix scroll position restore issue after browser back button - * For custom table list view in Exment - */ -(function() { - 'use strict'; - - const CLASSNAME_CUSTOM_VALUE_GRID = 'block_custom_value_grid'; - - // Save current scroll position - function saveCurrentScrollPosition() { - const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; - - // Store in history state if available - if (window.history && window.history.replaceState) { - const currentState = window.history.state || {}; - currentState.scrollTop = scrollTop; - try { - window.history.replaceState(currentState, document.title); - } catch (e) { - console.warn('Failed to update history state:', e); - } - } - } - - // Restore scroll position from history state - function restoreScrollPosition() { - let scrollTop = 0; - - // Only restore from history state (for back button) - if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { - scrollTop = window.history.state.scrollTop; - } - - if (scrollTop > 0) { - // Use setTimeout to ensure DOM is ready - setTimeout(function() { - window.scrollTo(0, scrollTop); - // Verify and retry if needed - setTimeout(function() { - const currentScroll = window.pageYOffset || document.documentElement.scrollTop; - if (Math.abs(currentScroll - scrollTop) > 50) { - window.scrollTo(0, scrollTop); - } - }, 100); - }, 0); - } - } - - // Initialize on document ready - $(function() { - // Prevent multiple initialization - if (window.exmentScrollFixInitialized) { - return; - } - window.exmentScrollFixInitialized = true; - - // Disable browser's default scroll restoration - if ('scrollRestoration' in history) { - history.scrollRestoration = 'manual'; - } - - restoreScrollPosition(); - - // Save scroll position periodically while scrolling - let scrollTimer; - $(window).on('scroll', function() { - clearTimeout(scrollTimer); - scrollTimer = setTimeout(function() { - // Only save if we're on a list page (grid view) - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - saveCurrentScrollPosition(); - } - }, 150); - }); - - // Save scroll position before leaving page - $(window).on('beforeunload', function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - saveCurrentScrollPosition(); - } - }); - - // Handle pjax events - if ($.pjax) { - let isPjaxPopstate = false; - - $(document).on('pjax:popstate', function() { - isPjaxPopstate = true; - }); - - // Save scroll position before pjax request - $(document).on('pjax:send', function(event, xhr, options) { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - saveCurrentScrollPosition(); - } - }); - - // Handle scroll position after pjax complete - $(document).on('pjax:end', function(event, xhr, options) { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - if (isPjaxPopstate) { - // If it's a back/forward navigation, restore scroll - restoreScrollPosition(); - isPjaxPopstate = false; - } else { - // If it's a new navigation (pagination, sort, etc), scroll to top - window.scrollTo(0, 0); - } - } - }); - } - - // Save position when clicking on links in grid - $('.' + CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { - saveCurrentScrollPosition(); - }); - }); -})(); diff --git a/public/vendor/exment/js/scroll-restore.js b/public/vendor/exment/js/scroll-restore.js new file mode 100644 index 000000000..368ca822b --- /dev/null +++ b/public/vendor/exment/js/scroll-restore.js @@ -0,0 +1,128 @@ +/** + * Scroll position restore for custom table list view + * Handles scroll restoration on browser back button and pjax navigation + */ +var Exment; +(function (Exment) { + 'use strict'; + + const CLASSNAME_CUSTOM_VALUE_GRID = '.block_custom_value_grid'; + + // Save current scroll position + function saveCurrentScrollPosition() { + const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; + + // Store in history state if available + if (window.history && window.history.replaceState) { + const currentState = window.history.state || {}; + currentState.scrollTop = scrollTop; + try { + window.history.replaceState(currentState, document.title); + } catch (e) { + console.warn('Failed to update history state:', e); + } + } + } + + // Restore scroll position from history state + function restoreScrollPosition() { + let scrollTop = 0; + + // Only restore from history state (for back button) + if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { + scrollTop = window.history.state.scrollTop; + } + + if (scrollTop > 0) { + // Use setTimeout to ensure DOM is ready + setTimeout(function() { + window.scrollTo(0, scrollTop); + // Verify and retry if needed + setTimeout(function() { + const currentScroll = window.pageYOffset || document.documentElement.scrollTop; + if (Math.abs(currentScroll - scrollTop) > 50) { + window.scrollTo(0, scrollTop); + } + }, 100); + }, 0); + } + } + + // ScrollRestore module + Exment.ScrollRestore = { + init: function() { + // Prevent multiple initialization + if (window.exmentScrollRestoreInitialized) { + return; + } + window.exmentScrollRestoreInitialized = true; + + // Disable browser's default scroll restoration + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + + restoreScrollPosition(); + + // Save scroll position periodically while scrolling + let scrollTimer; + $(window).on('scroll', function() { + clearTimeout(scrollTimer); + scrollTimer = setTimeout(function() { + // Only save if we're on a list page (grid view) + if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }, 150); + }); + + // Save scroll position before leaving page + $(window).on('beforeunload', function() { + if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Handle pjax events + if ($.pjax) { + let isPjaxPopstate = false; + + $(document).on('pjax:popstate', function() { + isPjaxPopstate = true; + }); + + // Save scroll position before pjax request + $(document).on('pjax:send', function(event, xhr, options) { + if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Handle scroll position after pjax complete + $(document).on('pjax:end', function(event, xhr, options) { + if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + if (isPjaxPopstate) { + // If it's a back/forward navigation, restore scroll + restoreScrollPosition(); + isPjaxPopstate = false; + } else { + // If it's a new navigation (pagination, sort, etc), scroll to top + window.scrollTo(0, 0); + } + } + }); + } + + // Save position when clicking on links in grid + $(CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { + saveCurrentScrollPosition(); + }); + } + }; + + // Auto-initialize on document ready + $(function() { + Exment.ScrollRestore.init(); + }); + +})(Exment || (Exment = {})); diff --git a/src/Middleware/Bootstrap.php b/src/Middleware/Bootstrap.php index 507ba13d0..e15b08037 100644 --- a/src/Middleware/Bootstrap.php +++ b/src/Middleware/Bootstrap.php @@ -77,7 +77,7 @@ protected function setCssJs(Request $request, \Closure $next) 'vendor/exment/jstree/jstree.min.js', 'vendor/exment/js/common_all.js', 'vendor/exment/js/common.js', - 'vendor/exment/js/scroll-position-fix.js', + 'vendor/exment/js/scroll-restore.js', 'vendor/exment/js/search.js', 'vendor/exment/js/calc.js', 'vendor/exment/js/notify_navbar.js',