├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── Blazor.IntersectionObserver.sln ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENCE.txt ├── README.md ├── codecoverage.runsettings ├── samples ├── Blazor.IntersectionObserver.Client │ ├── App.razor │ ├── Blazor.IntersectionObserver.Client.csproj │ ├── Pages │ │ ├── Index.razor │ │ └── LazyImages.razor │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── _Imports.razor │ └── wwwroot │ │ ├── css │ │ ├── app.css │ │ └── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ ├── favicon.ico │ │ └── index.html └── Blazor.IntersectionObserver.Server │ ├── App.razor │ ├── Blazor.IntersectionObserver.Server.csproj │ ├── Data │ ├── WeatherForecast.cs │ └── WeatherForecastService.cs │ ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.razor │ ├── LazyImages.razor │ └── _Host.cshtml │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css │ ├── Startup.cs │ ├── _Imports.razor │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ └── site.css │ └── favicon.ico └── src └── Ljbc1994.Blazor.IntersectionObserver ├── API ├── DOMRectReadOnly.cs ├── ForwardReference.cs └── IntersectionObserverEntry.cs ├── Configuration └── Constants.cs ├── IIntersectionObserverService.cs ├── IntersectionObserve.cs ├── IntersectionObserver.cs ├── IntersectionObserverContext.cs ├── IntersectionObserverExtensions.cs ├── IntersectionObserverOptions.cs ├── IntersectionObserverService.cs ├── Ljbc1994.Blazor.IntersectionObserver.csproj ├── Properties └── launchSettings.json ├── _Imports.razor ├── __tests__ ├── data │ └── index.ts ├── index.test.ts └── utils │ ├── config.ts │ ├── document.ts │ └── iterable.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.ts ├── tsconfig.json └── wwwroot ├── blazor-intersection-observer.js └── blazor-intersection-observer.min.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | # This file is the top-most EditorConfig file 4 | root = true 5 | 6 | # All Files 7 | [*] 8 | charset = utf-8 9 | end_of_line = crlf 10 | indent_style = space 11 | indent_size = 4 12 | insert_final_newline = false 13 | trim_trailing_whitespace = true 14 | 15 | # Solution Files 16 | [*.sln] 17 | indent_style = tab 18 | 19 | # XML Project Files 20 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 21 | indent_size = 2 22 | 23 | # Configuration Files 24 | [*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] 25 | indent_size = 2 26 | 27 | # Markdown Files 28 | [*.md] 29 | trim_trailing_whitespace = false 30 | 31 | # Web Files 32 | [*.{htm,html,js,ts,css,scss,less}] 33 | indent_size = 2 34 | insert_final_newline = true 35 | 36 | # Bash Files 37 | [*.sh] 38 | end_of_line = lf 39 | 40 | # Dotnet Code Style Settings 41 | # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 42 | # See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers 43 | [*.{cs,csx,cake,vb}] 44 | dotnet_sort_system_directives_first = true:warning 45 | dotnet_style_coalesce_expression = true:warning 46 | dotnet_style_collection_initializer = true:warning 47 | dotnet_style_explicit_tuple_names = true:warning 48 | dotnet_style_null_propagation = true:warning 49 | dotnet_style_object_initializer = true:warning 50 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 51 | dotnet_style_predefined_type_for_member_access = true:warning 52 | dotnet_style_qualification_for_event = true:warning 53 | dotnet_style_qualification_for_field = true:warning 54 | dotnet_style_qualification_for_method = true:warning 55 | dotnet_style_qualification_for_property = true:warning 56 | 57 | # Naming Symbols 58 | # constant_fields - Define constant fields 59 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 60 | dotnet_naming_symbols.constant_fields.required_modifiers = const 61 | # non_private_readonly_fields - Define public, internal and protected readonly fields 62 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected 63 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 64 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 65 | # static_readonly_fields - Define static and readonly fields 66 | dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field 67 | dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly 68 | # private_readonly_fields - Define private readonly fields 69 | dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private 70 | dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field 71 | dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly 72 | # public_internal_fields - Define public and internal fields 73 | dotnet_naming_symbols.public_internal_fields.applicable_accessibilities = public, internal 74 | dotnet_naming_symbols.public_internal_fields.applicable_kinds = field 75 | # private_protected_fields - Define private and protected fields 76 | dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected 77 | dotnet_naming_symbols.private_protected_fields.applicable_kinds = field 78 | # public_symbols - Define any public symbol 79 | dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal 80 | dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate 81 | # parameters - Defines any parameter 82 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 83 | # non_interface_types - Defines class, struct, enum and delegate types 84 | dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate 85 | # interface_types - Defines interfaces 86 | dotnet_naming_symbols.interface_types.applicable_kinds = interface 87 | 88 | # Naming Styles 89 | # camel_case - Define the camelCase style 90 | dotnet_naming_style.camel_case.capitalization = camel_case 91 | # pascal_case - Define the Pascal_case style 92 | dotnet_naming_style.pascal_case.capitalization = pascal_case 93 | # first_upper - The first character must start with an upper-case character 94 | dotnet_naming_style.first_upper.capitalization = first_word_upper 95 | # prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I' 96 | dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case 97 | dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I 98 | 99 | # Naming Rules 100 | # Constant fields must be PascalCase 101 | dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = warning 102 | dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields 103 | dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case 104 | # Public, internal and protected readonly fields must be PascalCase 105 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = warning 106 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields 107 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case 108 | # Static readonly fields must be PascalCase 109 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning 110 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields 111 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case 112 | # Private readonly fields must be camelCase 113 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning 114 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields 115 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case 116 | # Public and internal fields must be PascalCase 117 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning 118 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields 119 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case 120 | # Private and protected fields must be camelCase 121 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning 122 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields 123 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case 124 | # Public members must be capitalized 125 | dotnet_naming_rule.public_members_must_be_capitalized.severity = warning 126 | dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols 127 | dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper 128 | # Parameters must be camelCase 129 | dotnet_naming_rule.parameters_must_be_camel_case.severity = warning 130 | dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters 131 | dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case 132 | # Class, struct, enum and delegates must be PascalCase 133 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = warning 134 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types 135 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case 136 | # Interfaces must be PascalCase and start with an 'I' 137 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning 138 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types 139 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i 140 | 141 | # C# Code Style Settings 142 | # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 143 | # See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers 144 | [*.cs,csx,cake] 145 | # Indentation Options 146 | csharp_indent_block_contents = true:warning 147 | csharp_indent_braces = false:warning 148 | csharp_indent_case_contents = true:warning 149 | csharp_indent_labels = no_change:warning 150 | csharp_indent_switch_labels = true:warning 151 | # Style Options 152 | csharp_style_conditional_delegate_call = true:warning 153 | csharp_style_expression_bodied_accessors = true:warning 154 | csharp_style_expression_bodied_constructors = true:warning 155 | csharp_style_expression_bodied_indexers = true:warning 156 | csharp_style_expression_bodied_methods = true:warning 157 | csharp_style_expression_bodied_operators = true:warning 158 | csharp_style_expression_bodied_properties = true:warning 159 | csharp_style_inlined_variable_declaration = true:warning 160 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 161 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 162 | csharp_style_throw_expression = true:warning 163 | csharp_style_var_elsewhere = true:warning 164 | csharp_style_var_for_built_in_types = true:warning 165 | csharp_style_var_when_type_is_apparent = true:warning 166 | # New Line Options 167 | csharp_new_line_before_catch = true:warning 168 | csharp_new_line_before_else = true:warning 169 | csharp_new_line_before_finally = true:warning 170 | csharp_new_line_before_members_in_anonymous_types = true:warning 171 | csharp_new_line_before_members_in_object_initializers = true:warning 172 | # BUG: Warning level cannot be set https://github.com/dotnet/roslyn/issues/18010 173 | csharp_new_line_before_open_brace = all 174 | csharp_new_line_between_query_expression_clauses = true:warning 175 | # Spacing Options 176 | csharp_space_after_cast = false:warning 177 | csharp_space_after_colon_in_inheritance_clause = true:warning 178 | csharp_space_after_comma = true:warning 179 | csharp_space_after_dot = false:warning 180 | csharp_space_after_keywords_in_control_flow_statements = true:warning 181 | csharp_space_after_semicolon_in_for_statement = true:warning 182 | csharp_space_around_binary_operators = before_and_after:warning 183 | csharp_space_around_declaration_statements = do_not_ignore:warning 184 | csharp_space_before_colon_in_inheritance_clause = true:warning 185 | csharp_space_before_comma = false:warning 186 | csharp_space_before_dot = false:warning 187 | csharp_space_before_semicolon_in_for_statement = false:warning 188 | csharp_space_before_open_square_brackets = false:warning 189 | csharp_space_between_empty_square_brackets = false:warning 190 | csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning 191 | csharp_space_between_method_declaration_parameter_list_parentheses = false:warning 192 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning 193 | csharp_space_between_method_call_name_and_opening_parenthesis = false:warning 194 | csharp_space_between_method_call_parameter_list_parentheses = false:warning 195 | csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning 196 | csharp_space_between_parentheses = expressions:warning 197 | csharp_space_between_square_brackets = false:warning 198 | # Wrapping Options 199 | csharp_preserve_single_line_blocks = true:warning 200 | csharp_preserve_single_line_statements = false:warning -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Please provide a minimal working example and/or steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Relevant information (please complete the following information):** 24 | - Dotnet Version: [e.g. dotnet6.0] 25 | - Blazor WebAssembly or Server 26 | 27 | **Additional information (please complete the following information):** 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | 34 | **Screenshots** 35 | If applicable, add screenshots to help explain your problem. 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | paths-ignore: 7 | - '**.md' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 10.x 19 | 20 | - name: Install npm dependencies 21 | run: | 22 | npm ci 23 | working-directory: src/Ljbc1994.Blazor.IntersectionObserver 24 | 25 | - name: Run tests 26 | run: | 27 | npm run test 28 | working-directory: src/Ljbc1994.Blazor.IntersectionObserver 29 | 30 | - name: Setup Dotnet 31 | uses: actions/setup-dotnet@v1 32 | with: 33 | dotnet-version: '5.0.x' 34 | include-prerelease: true 35 | 36 | - name: Dotnet build 37 | run: dotnet build --configuration Release 38 | 39 | - name: Dotnet pack 40 | run: dotnet pack src/Ljbc1994.Blazor.IntersectionObserver/*.csproj --configuration Release --verbosity Detailed 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths-ignore: 7 | - '**.md' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 10.x 19 | 20 | - name: Install npm dependencies 21 | run: | 22 | npm ci 23 | working-directory: src/Ljbc1994.Blazor.IntersectionObserver 24 | 25 | - name: Run tests 26 | run: | 27 | npm run test 28 | working-directory: src/Ljbc1994.Blazor.IntersectionObserver 29 | 30 | - name: Setup Dotnet 31 | uses: actions/setup-dotnet@v1 32 | with: 33 | dotnet-version: '5.0.x' 34 | include-prerelease: true 35 | 36 | - name: Dotnet build 37 | run: dotnet build --configuration Release 38 | 39 | - name: Dotnet pack 40 | run: dotnet pack src/Ljbc1994.Blazor.IntersectionObserver/*.csproj --configuration Release --verbosity Detailed 41 | 42 | - name: Dotnet publish to nuget 43 | run: dotnet nuget push src/Ljbc1994.Blazor.IntersectionObserver/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols true 44 | env: 45 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }} 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | #Test 7 | coverage 8 | junit.xml 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUNIT 48 | *.VisualState.xml 49 | TestResult.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | 215 | # Visual Studio cache files 216 | # files ending in .cache can be ignored 217 | *.[Cc]ache 218 | # but keep track of directories ending in .cache 219 | !?*.[Cc]ache/ 220 | 221 | # Others 222 | ClientBin/ 223 | ~$* 224 | *~ 225 | *.dbmdl 226 | *.dbproj.schemaview 227 | *.jfm 228 | *.pfx 229 | *.publishsettings 230 | orleans.codegen.cs 231 | 232 | # Including strong name files can present a security risk 233 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 234 | #*.snk 235 | 236 | # Since there are multiple workflows, uncomment next line to ignore bower_components 237 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 238 | #bower_components/ 239 | 240 | # RIA/Silverlight projects 241 | Generated_Code/ 242 | 243 | # Backup & report files from converting an old project file 244 | # to a newer Visual Studio version. Backup files are not needed, 245 | # because we have git ;-) 246 | _UpgradeReport_Files/ 247 | Backup*/ 248 | UpgradeLog*.XML 249 | UpgradeLog*.htm 250 | ServiceFabricBackup/ 251 | *.rptproj.bak 252 | 253 | # SQL Server files 254 | *.mdf 255 | *.ldf 256 | *.ndf 257 | 258 | # Business Intelligence projects 259 | *.rdl.data 260 | *.bim.layout 261 | *.bim_*.settings 262 | *.rptproj.rsuser 263 | *- Backup*.rdl 264 | 265 | # Microsoft Fakes 266 | FakesAssemblies/ 267 | 268 | # GhostDoc plugin setting file 269 | *.GhostDoc.xml 270 | 271 | # Node.js Tools for Visual Studio 272 | .ntvs_analysis.dat 273 | node_modules/ 274 | 275 | # Visual Studio 6 build log 276 | *.plg 277 | 278 | # Visual Studio 6 workspace options file 279 | *.opt 280 | 281 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 282 | *.vbw 283 | 284 | # Visual Studio LightSwitch build output 285 | **/*.HTMLClient/GeneratedArtifacts 286 | **/*.DesktopClient/GeneratedArtifacts 287 | **/*.DesktopClient/ModelManifest.xml 288 | **/*.Server/GeneratedArtifacts 289 | **/*.Server/ModelManifest.xml 290 | _Pvt_Extensions 291 | 292 | # Paket dependency manager 293 | .paket/paket.exe 294 | paket-files/ 295 | 296 | # FAKE - F# Make 297 | .fake/ 298 | 299 | # JetBrains Rider 300 | .idea/ 301 | *.sln.iml 302 | 303 | # CodeRush personal settings 304 | .cr/personal 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # Local History for Visual Studio 342 | .localhistory/ 343 | 344 | # BeatPulse healthcheck temp database 345 | healthchecksdb -------------------------------------------------------------------------------- /Blazor.IntersectionObserver.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.452 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{06DF5E56-1E2C-4435-854E-0155228D42E6}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D04FCF1-FEF2-4F22-BF8A-5ACD34C364D7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.IntersectionObserver.Client", "samples\Blazor.IntersectionObserver.Client\Blazor.IntersectionObserver.Client.csproj", "{F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazor.IntersectionObserver.Server", "samples\Blazor.IntersectionObserver.Server\Blazor.IntersectionObserver.Server.csproj", "{8F503EA1-622E-48C0-8BA4-51AE7D413697}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ljbc1994.Blazor.IntersectionObserver", "src\Ljbc1994.Blazor.IntersectionObserver\Ljbc1994.Blazor.IntersectionObserver.csproj", "{5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {8F503EA1-622E-48C0-8BA4-51AE7D413697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {8F503EA1-622E-48C0-8BA4-51AE7D413697}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {8F503EA1-622E-48C0-8BA4-51AE7D413697}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8F503EA1-622E-48C0-8BA4-51AE7D413697}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(NestedProjects) = preSolution 39 | {F3CCFB30-6C8F-40A8-BF19-1E19A17DBF80} = {5D04FCF1-FEF2-4F22-BF8A-5ACD34C364D7} 40 | {8F503EA1-622E-48C0-8BA4-51AE7D413697} = {5D04FCF1-FEF2-4F22-BF8A-5ACD34C364D7} 41 | {5EAE5F4C-24EF-45A4-B4BE-D5C24D5772ED} = {06DF5E56-1E2C-4435-854E-0155228D42E6} 42 | EndGlobalSection 43 | GlobalSection(ExtensibilityGlobals) = postSolution 44 | SolutionGuid = {2A1E800E-0698-42F3-8B5D-2788225893E5} 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ljbc94@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Louie Colgan 5 | Ljbc1994.Blazor.IntersectionObserver 6 | BlazorIntersectionObserver 7 | Copyright © 2021 - Louie Colgan 8 | Intersection Observer API wrapper for Blazor 9 | https://github.com/ljbc1994/BlazorIntersectionObserver 10 | Microsoft ASP.NET Core Blazor Intersection Observer 11 | https://github.com/ljbc1994/BlazorIntersectionObserver 12 | https://github.com/ljbc1994/BlazorIntersectionObserver/blob/master/LICENCE 13 | git 14 | true 15 | 16 | 17 | 18 | 19 | 20 | latest 21 | true 22 | true 23 | 24 | full 25 | 26 | 27 | 28 | net5.0 29 | 8.0 30 | 3.0 31 | true 32 | 33 | 34 | 35 | 36 | 3.1.0 37 | dev 38 | 39 | 40 | 41 | 42 | 46 | 47 | <developer build> 48 | 49 | 50 | 51 | 52 | $(BUILD_SOURCEVERSION) 53 | 54 | 55 | 56 | 57 | $(GIT_COMMIT) 58 | 59 | 60 | 61 | 62 | Not found 63 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory).git')) 64 | $([System.IO.File]::ReadAllText('$(DotGitDir)/HEAD').Trim()) 65 | $(DotGitDir)/$(HeadFileContent.Substring(5)) 66 | $([System.IO.File]::ReadAllText('$(RefPath)').Trim()) 67 | $(HeadFileContent) 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(Version). Commit Hash: $(GitHeadSha) 5 | 6 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2021 Louie Colgan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlazorIntersectionObserver 2 | 3 | [![Package Version](https://img.shields.io/nuget/v/BlazorIntersectionObserver.svg)](https://www.nuget.org/packages/BlazorIntersectionObserver) 4 | [![NuGet Downloads](https://img.shields.io/nuget/dt/BlazorIntersectionObserver.svg)](https://www.nuget.org/packages/BlazorIntersectionObserver) 5 | [![License](https://img.shields.io/github/license/ljbc1994/BlazorIntersectionObserver.svg)](https://github.com/ljbc1994/BlazorIntersectionObserver/blob/master/LICENCE) 6 | 7 | > A comprehensive wrapper around the Intersection Observer API, giving you all the goodness of observing intersections in a performant way. 8 | 9 | This is a wrapper around the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) so that you can use it in `Blazor` for .NET 5. It has the same API structure with convenience methods and components for a better dev experience. It works with both Blazor WebAssembly and Blazor Server. 10 | 11 | ## Get Started 12 | 13 | ### 1. Installation 14 | 15 | Install `BlazorIntersectionObserver` through NuGet. 16 | 17 | ```bash 18 | > dotnet add package BlazorIntersectionObserver 19 | ``` 20 | 21 | ### 2. Register the service 22 | 23 | Now you'll need to add the service to the service configuration. 24 | 25 | #### WebAssembly (Program.cs) 26 | 27 | ```cs 28 | using Ljbc1994.Blazor.IntersectionObserver; 29 | 30 | public class Program 31 | { 32 | public static async Task Main(string[] args) 33 | { 34 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 35 | builder.Services.AddIntersectionObserver(); 36 | } 37 | } 38 | ``` 39 | 40 | *OR* 41 | 42 | #### Server (Startup.cs) 43 | 44 | ```cs 45 | using Ljbc1994.Blazor.IntersectionObserver; 46 | 47 | public class Startup { 48 | public void ConfigureServices(IServiceCollection services) 49 | { 50 | services.AddIntersectionObserver(); 51 | } 52 | } 53 | ``` 54 | 55 | ### 3. Use it! 56 | 57 | For the quickest setup, use the `IntersectionObserve` component. This provides an implicit context 58 | object which contains the observer entry! Easy! 59 | 60 | #### Component setup 61 | 62 | ```razor 63 | @using Ljbc1994.Blazor.IntersectionObserver.Components 64 | 65 | 66 |
67 | Hey... I'm @(context.IsIntersecting ? "in view": "out of view") 68 |
69 |
70 | ``` 71 | 72 | *OR* 73 | 74 | #### Service setup 75 | 76 | To directly use the service, you just need to inject it and observe the element(s). 77 | 78 | ```razor 79 | @using Ljbc1994.Blazor.IntersectionObserver 80 | @inject IIntersectionObserverService ObserverService 81 | 82 | 83 | 84 | @functions { 85 | public ElementReference ImageElement { get; set; } 86 | public bool IsIntersecting { get; set; } 87 | 88 | protected override async Task OnAfterRenderAsync(bool firstRender) 89 | { 90 | if (firstRender) 91 | { 92 | await SetupObserver(); 93 | } 94 | } 95 | 96 | public async void SetupObserver() 97 | { 98 | await ObserverService.Observe(ImageElement, (entries) => 99 | { 100 | var entry = entries.FirstOrDefault(); 101 | IsIntersecting = entry.IsIntersecting; 102 | StateHasChanged(); 103 | }); 104 | } 105 | } 106 | ``` 107 | 108 | ## Documentation and Usage 109 | 110 | ### Options 111 | 112 | You can pass through `options` to the `ObserverService` methods, these are the same as the Intersection Observer API options. 113 | 114 | #### Example 115 | 116 | ```cs 117 | var options = new IntersectionObserverOptions { 118 | Root = BodyRef, 119 | Threshold = new List { 0.25, 0.5, 1 }, 120 | RootMargin = "10px 10px 10px 10px" 121 | }; 122 | ``` 123 | 124 | ### Service Methods 125 | 126 | #### `Observe` 127 | 128 | This a shorthand way of observing an element by providing: 129 | - The element you want to observe. 130 | - The callback to trigger on an intersection update. 131 | - The intersection observer options. 132 | 133 | This returns an `IntersectionObserver` instance, allowing you to `disconnect` the observer or `unobserve` an element. Or if you wish, observe additional elements. 134 | 135 | ```cs 136 | var observer = await ObserverService.Observe(ElementRef, (entries) => { 137 | IsIntersecting = entries.FirstOrDefault().IsIntersecting; 138 | StateHasChanged(); 139 | }, options); 140 | ``` 141 | 142 | #### `Create` 143 | 144 | The `Create` method follows the same approach as the Intersection Observer API, you create the observer and then pass elements you wish to observe by calling the `Observe` method on the observer instance. To create the observer, provide the following: 145 | 146 | - The callback to trigger on an intersection update. 147 | - The intersection observer options. 148 | 149 | This returns an `IntersectionObserver` instance, allowing you to `Observe` elements. This also provides the ability to `disconnect` or `unobserve` the element. 150 | 151 | ```cs 152 | var observer = await ObserverService.Create((entries) => { 153 | IsIntersecting = entries.FirstOrDefault().IsIntersecting; 154 | StateHasChanged(); 155 | }, options); 156 | 157 | await observer.Observe(FirstImage); 158 | await observer.Unobserve(FirstImage); 159 | ``` 160 | 161 | ### `IntersectionObserver` Methods 162 | 163 | #### `Observe` 164 | 165 | To observe an element, provide the element reference to the `IntersectionObserver` instance by calling `Observe`. 166 | 167 | ```cs 168 | observer.Observe(ElementReference); 169 | ``` 170 | 171 | #### `Unobserve` 172 | To unobserve an element, provide the element reference to the `IntersectionObserver` instance by calling `Unobserve`. 173 | 174 | ```cs 175 | observer.Unobserve(ElementReference); 176 | ``` 177 | 178 | #### `Disconnect` 179 | To disconnect the observer, call `Disconnect` on the `IntersectionObserver` instance. 180 | 181 | ```cs 182 | observer.Disconnect(); 183 | ``` 184 | 185 | This will remove all the observed elements from the observer, i.e. 186 | 187 | ```razor 188 | @using Ljbc1994.Blazor.IntersectionObserver 189 | @implements IAsyncDisposable 190 | @inject IIntersectionObserverService ObserverService 191 | 192 |
193 | 194 | @functions { 195 | private IntersectionObserver Observer; 196 | @* Code... *@ 197 | 198 | public async ValueTask DisconnectAll() 199 | { 200 | if (this.Observer != null) 201 | { 202 | await this.Observer.Disconnect(); 203 | } 204 | } 205 | } 206 | 207 | ``` 208 | 209 | #### `Dispose` 210 | To remove the observer, call `Dispose` on the `IntersectionObserver` instance. 211 | 212 | ```cs 213 | observer.Dispose(); 214 | ``` 215 | 216 | This is a useful method to clean up observers when components are disposed of, i.e. 217 | 218 | ```razor 219 | @using Ljbc1994.Blazor.IntersectionObserver 220 | @implements IAsyncDisposable 221 | @inject IIntersectionObserverService ObserverService 222 | 223 |
224 | 225 | @functions { 226 | private IntersectionObserver Observer; 227 | @* Code... *@ 228 | 229 | public async ValueTask DisposeAsync() 230 | { 231 | if (this.Observer != null) 232 | { 233 | await this.Observer.Dispose(); 234 | } 235 | } 236 | } 237 | 238 | ``` 239 | 240 | ### Component 241 | 242 | #### `` 243 | 244 | Rather than directly interfacing with the service, you can use this convenience component for quick and easy observing. You can access the observer entry through the implicit `@context`! 245 | 246 | You need to make sure to provide the reference of the element you want to observe, this is done by passing the element reference to the context reference. 247 | 248 | ```razor 249 | @* Injecting service... *@ 250 | 251 | 252 |
253 | Hey... look I'm @(context.IsIntersecting ? "intersecting!": "not intersecting!") 254 |
255 |
256 | 257 | @* Component code... *@ 258 | ``` 259 | 260 | ##### Props 261 | 262 | - `OnChange` (`EventCallback`) - When the intersection observer has a entry update. 263 | - `IsIntersecting` (`bool`) - Whether the element is intersecting - used for two-way binding. 264 | - `Options` (`IntersectionObserverOptions`) - The options for the observer. 265 | - `Once` (`bool`) - Only observe once for an intersection, then the instance disposes of itself. 266 | 267 | #### Context 268 | The context is the `IntersectionObserverContext` object, with the following signature: 269 | 270 | ```cs 271 | public class IntersectionObserverContext 272 | { 273 | public IntersectionObserverEntry Entry { get; set; } 274 | public ForwardReference Ref { get; set; } = new ForwardReference(); 275 | public bool IsIntersecting => this.Entry?.IsIntersecting ?? false; 276 | } 277 | 278 | public class IntersectionObserverEntry 279 | { 280 | public bool IsIntersecting { get; set; } 281 | 282 | public double IntersectionRatio { get; set; } 283 | 284 | public DOMRectReadOnly BoundingClientRect { get; set; } 285 | 286 | public DOMRectReadOnly RootBounds { get; set; } 287 | 288 | public bool IsVisible { get; set; } 289 | 290 | public double Time { get; set; } 291 | } 292 | ``` 293 | 294 | ## Additional Information 295 | 296 | ### Upgrading to `2.0.1`+ 297 | 298 | In versions prior to `2.0.1`, the `IntersectionObserve` component didn't require a reference to the node as it was wrapped in an element that was automatically observed. This was changed to ensure the consumer provides the reference to prevent any potential layout issues and make it explicit what element should be observed. 299 | 300 | Therefore, before `2.0.1`, if the consumer had an element with `display: none;` within the `IntersectionObserve` component, this would have worked. However, as we're now observing the element provided as opposed to a wrapped element, this will no longer work. To resolve this, you can wrap the observed element in a div and observe the container div instead of the observed element. 301 | 302 | ## Feature Requests 303 | There's so much that `IntersectionObserver` can do, so if you have any requests or you want better documentation and examples, feel free to make a pull request or create an issue! 304 | 305 | 306 | 307 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /codecoverage.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | .*.testadapter.dll 11 | .*.test.dll 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Blazor.IntersectionObserver.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | BlazorIntersectionObserverClient 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using Ljbc1994.Blazor.IntersectionObserver 3 | @using Ljbc1994.Blazor.IntersectionObserver.API 4 | @using Ljbc1994.Blazor.IntersectionObserver.Components 5 | 6 |
7 |
8 |

9 | Mozilla Example Implementation 10 |

11 |

12 | This example matches the mozilla example. This simple example causes a target element to change its color and transparency as it becomes more or less visible. Scroll down to see! 13 |

14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 | Welcome to The Box! 22 |
23 |
24 |
25 |
26 | 27 | @functions { 28 | public static string IncreasingColor = "rgba(40, 40, 190, {0})"; 29 | public static string DecreasingColor = "rgba(190, 40, 40, {0})"; 30 | 31 | public static List BuildThresholdList() 32 | { 33 | var numberOfSteps = 20; 34 | 35 | var ratios = Enumerable 36 | .Range(1, numberOfSteps) 37 | .Select(x => (double)x / numberOfSteps) 38 | .ToList(); 39 | 40 | ratios.Add(0); 41 | 42 | return ratios; 43 | } 44 | 45 | private double PreviousRatio { get; set; } = 0; 46 | 47 | public string BoxStyle { get; set; } = ""; 48 | 49 | public readonly IntersectionObserverOptions ObserverOptions = new IntersectionObserverOptions 50 | { 51 | RootMargin = "0px", 52 | Threshold = BuildThresholdList() 53 | }; 54 | 55 | public void OnIntersectingChanged(IntersectionObserverEntry entry) 56 | { 57 | if (entry.IntersectionRatio > PreviousRatio) 58 | { 59 | BoxStyle = $"background-color:{string.Format(IncreasingColor, entry.IntersectionRatio)}"; 60 | } 61 | else 62 | { 63 | BoxStyle = $"background-color:{string.Format(DecreasingColor, entry.IntersectionRatio)}"; 64 | } 65 | 66 | PreviousRatio = entry.IntersectionRatio; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor: -------------------------------------------------------------------------------- 1 | @page "/lazy-images" 2 | 3 | @using Ljbc1994.Blazor.IntersectionObserver.Components 4 | 5 |
6 |
7 |

8 | Lazy Image Loading 9 |

10 |

11 | This example uses the IntersectionObserve component to check whether the element is 12 | in view before attempting to load the image. 13 |

14 |
15 |
16 | 17 |
18 |
19 |
20 | @foreach (var image in Images) 21 | { 22 |
23 | 24 |
25 | @{var isIntersecting = context.IsIntersecting;} 26 |
Loading...
27 | 28 |
29 |
30 |
31 |
Card title
32 |

Some quick example text to build on the card title and make up the bulk of the card's content.

33 |
34 |
35 | } 36 |
37 |
38 |
39 | 40 | @functions { 41 | public static int RandomNumber(int min, int max) 42 | { 43 | return new Random().Next(min, max); 44 | } 45 | 46 | public static List Images = Enumerable 47 | .Range(1, 20) 48 | .Select(x => 49 | { 50 | var height = RandomNumber(250, 650); 51 | var width = RandomNumber(250, 650); 52 | return $"https://picsum.photos/{width}/{height}?id=${x}"; 53 | }) 54 | .ToList(); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.Http; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Ljbc1994.Blazor.IntersectionObserver; 11 | 12 | namespace Blazor.IntersectionObserver.Client 13 | { 14 | public class Program 15 | { 16 | public static async Task Main(string[] args) 17 | { 18 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 19 | builder.RootComponents.Add("#app"); 20 | 21 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 22 | 23 | builder.Services.AddIntersectionObserver(); 24 | 25 | await builder.Build().RunAsync(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:61506", 7 | "sslPort": 44309 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Blazor.IntersectionObserver.Client": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": "true", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 5 | 6 |
7 |
8 | @Body 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 21 | 22 | @code { 23 | private bool collapseNavMenu = true; 24 | 25 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 26 | 27 | private void ToggleNavMenu() 28 | { 29 | collapseNavMenu = !collapseNavMenu; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Blazor.IntersectionObserver.Client 10 | @using Blazor.IntersectionObserver.Client.Shared 11 | 12 | @using Ljbc1994.Blazor.IntersectionObserver.API 13 | @using Ljbc1994.Blazor.IntersectionObserver.Components -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #0366d6; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .content { 16 | padding-top: 1.1rem; 17 | } 18 | 19 | .valid.modified:not([type=checkbox]) { 20 | outline: 1px solid #26b050; 21 | } 22 | 23 | .invalid { 24 | outline: 1px solid red; 25 | } 26 | 27 | .validation-message { 28 | color: red; 29 | } 30 | 31 | #blazor-error-ui { 32 | background: lightyellow; 33 | bottom: 0; 34 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 35 | display: none; 36 | left: 0; 37 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 38 | position: fixed; 39 | width: 100%; 40 | z-index: 1000; 41 | } 42 | 43 | #blazor-error-ui .dismiss { 44 | cursor: pointer; 45 | position: absolute; 46 | right: 0.75rem; 47 | top: 0.5rem; 48 | } 49 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blazor.IntersectionObserver.Client 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Loading...
15 | 16 |
17 | An unhandled error has occurred. 18 | Reload 19 | 🗙 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Blazor.IntersectionObserver.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | BlazorIntersectionObserverServer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Data/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.IntersectionObserver.Server.Data 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Data/WeatherForecastService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blazor.IntersectionObserver.Server.Data 6 | { 7 | public class WeatherForecastService 8 | { 9 | private static readonly string[] Summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | public Task GetForecastAsync(DateTime startDate) 15 | { 16 | var rng = new Random(); 17 | return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast 18 | { 19 | Date = startDate.AddDays(index), 20 | TemperatureC = rng.Next(-20, 55), 21 | Summary = Summaries[rng.Next(Summaries.Length)] 22 | }).ToArray()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Blazor.IntersectionObserver.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Blazor.IntersectionObserver.Server.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | [IgnoreAntiforgeryToken] 14 | public class ErrorModel : PageModel 15 | { 16 | public string RequestId { get; set; } 17 | 18 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 19 | 20 | private readonly ILogger _logger; 21 | 22 | public ErrorModel(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | public void OnGet() 28 | { 29 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using Ljbc1994.Blazor.IntersectionObserver 3 | @using Ljbc1994.Blazor.IntersectionObserver.API 4 | @using Ljbc1994.Blazor.IntersectionObserver.Components 5 | 6 |
7 |
8 |

9 | Mozilla Example Implementation 10 |

11 |

12 | This example matches the mozilla example. This simple example causes a target element to change its color and transparency as it becomes more or less visible. Scroll down to see! 13 |

14 |
15 |
16 | 17 |
18 | 19 |
20 | Welcome to The Box! 21 |
22 |
23 |
24 | 25 | @functions { 26 | public static string IncreasingColor = "rgba(40, 40, 190, {0})"; 27 | public static string DecreasingColor = "rgba(190, 40, 40, {0})"; 28 | 29 | public static List BuildThresholdList() 30 | { 31 | var numberOfSteps = 20; 32 | 33 | var ratios = Enumerable 34 | .Range(1, numberOfSteps) 35 | .Select(x => (double)x / numberOfSteps) 36 | .ToList(); 37 | 38 | ratios.Add(0); 39 | 40 | return ratios; 41 | } 42 | 43 | private double PreviousRatio { get; set; } = 0; 44 | 45 | public string BoxStyle { get; set; } = ""; 46 | 47 | public readonly IntersectionObserverOptions ObserverOptions = new IntersectionObserverOptions 48 | { 49 | RootMargin = "0px", 50 | Threshold = BuildThresholdList() 51 | }; 52 | 53 | public void OnIntersectingChanged(IntersectionObserverEntry entry) 54 | { 55 | if (entry.IntersectionRatio > PreviousRatio) 56 | { 57 | BoxStyle = $"background-color:{string.Format(IncreasingColor, entry.IntersectionRatio)}"; 58 | } 59 | else 60 | { 61 | BoxStyle = $"background-color:{string.Format(DecreasingColor, entry.IntersectionRatio)}"; 62 | } 63 | 64 | PreviousRatio = entry.IntersectionRatio; 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor: -------------------------------------------------------------------------------- 1 | @page "/lazy-images" 2 | 3 | @using Ljbc1994.Blazor.IntersectionObserver.Components 4 | 5 |
6 |
7 |

8 | Lazy Image Loading 9 |

10 |

11 | This example uses the IntersectionObserve component to check whether the element is 12 | in view before attempting to load the image. 13 |

14 |
15 |
16 | 17 |
18 |
19 |
20 | @foreach (var image in Images) 21 | { 22 |
23 | 24 |
25 | @{var isIntersecting = context.IsIntersecting;} 26 |
Loading...
27 | 28 |
29 |
30 |
31 |
Card title
32 |

Some quick example text to build on the card title and make up the bulk of the card's content.

33 |
34 |
35 | } 36 |
37 |
38 |
39 | 40 | @functions { 41 | public static int RandomNumber(int min, int max) 42 | { 43 | return new Random().Next(min, max); 44 | } 45 | 46 | public static List Images = Enumerable 47 | .Range(1, 20) 48 | .Select(x => 49 | { 50 | var height = RandomNumber(250, 650); 51 | var width = RandomNumber(250, 650); 52 | return $"https://picsum.photos/{width}/{height}?id={x}"; 53 | }) 54 | .ToList(); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace Blazor.IntersectionObserver.Server.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | Blazor.IntersectionObserver.Server 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | An error has occurred. This application may no longer respond until reloaded. 24 | 25 | 26 | An unhandled exception has occurred. See browser dev tools for details. 27 | 28 | Reload 29 | 🗙 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Blazor.IntersectionObserver.Server 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:51153", 7 | "sslPort": 44398 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Blazor.IntersectionObserver.Server": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 5 | 6 |
7 |
8 | @Body 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 21 | 22 | @code { 23 | private bool collapseNavMenu = true; 24 | 25 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 26 | 27 | private void ToggleNavMenu() 28 | { 29 | collapseNavMenu = !collapseNavMenu; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using Blazor.IntersectionObserver.Server.Data; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.HttpsPolicy; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | using Ljbc1994.Blazor.IntersectionObserver; 14 | 15 | namespace Blazor.IntersectionObserver.Server 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddRazorPages(); 31 | services.AddServerSideBlazor(); 32 | 33 | services.AddIntersectionObserver(); 34 | 35 | services.AddSingleton(); 36 | 37 | } 38 | 39 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 40 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 41 | { 42 | if (env.IsDevelopment()) 43 | { 44 | app.UseDeveloperExceptionPage(); 45 | } 46 | else 47 | { 48 | app.UseExceptionHandler("/Error"); 49 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 50 | app.UseHsts(); 51 | } 52 | 53 | app.UseHttpsRedirection(); 54 | app.UseStaticFiles(); 55 | 56 | 57 | app.UseRouting(); 58 | 59 | app.UseEndpoints(endpoints => 60 | { 61 | endpoints.MapBlazorHub(); 62 | endpoints.MapFallbackToPage("/_Host"); 63 | }); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using Blazor.IntersectionObserver.Server 10 | @using Blazor.IntersectionObserver.Server.Shared 11 | 12 | @using Ljbc1994.Blazor.IntersectionObserver.API 13 | @using Ljbc1994.Blazor.IntersectionObserver.Components -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /samples/Blazor.IntersectionObserver.Server/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljbc1994/BlazorIntersectionObserver/cf81a8c7f16fe6f88ac97e46fccbfc56c0d9b325/samples/Blazor.IntersectionObserver.Server/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/API/DOMRectReadOnly.cs: -------------------------------------------------------------------------------- 1 | namespace Ljbc1994.Blazor.IntersectionObserver.API 2 | { 3 | public class DOMRectReadOnlyOptions 4 | { 5 | public double X { get; set; } 6 | 7 | public double Y { get; set; } 8 | 9 | public double Width { get; set; } 10 | 11 | public double Height { get; set; } 12 | } 13 | 14 | public class DOMRectReadOnly 15 | { 16 | public static DOMRectReadOnly FromRect(DOMRectReadOnlyOptions options) 17 | { 18 | return new DOMRectReadOnly(options); 19 | } 20 | 21 | public DOMRectReadOnly(double x, double y, double width, double height) 22 | { 23 | this.X = x; 24 | this.Y = y; 25 | this.Width = width; 26 | this.Height = height; 27 | } 28 | 29 | public DOMRectReadOnly(DOMRectReadOnlyOptions options) 30 | { 31 | this.X = options.X; 32 | this.Y = options.Y; 33 | this.Width = options.Width; 34 | this.Height = options.Height; 35 | } 36 | 37 | public DOMRectReadOnly() { } 38 | 39 | public double X { get; private set; } 40 | public double Y { get; private set; } 41 | public double Width { get; private set; } 42 | public double Height { get; private set; } 43 | public double Top { get; private set; } 44 | public double Right { get; private set; } 45 | public double Bottom { get; private set; } 46 | public double Left { get; private set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/API/ForwardReference.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Ljbc1994.Blazor.IntersectionObserver.API 4 | { 5 | public class ForwardReference 6 | { 7 | private ElementReference current; 8 | 9 | public ElementReference Current 10 | { 11 | get => this.current; 12 | set => this.Set(value); 13 | } 14 | 15 | public void Set(ElementReference value) 16 | { 17 | this.current = value; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/API/IntersectionObserverEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Ljbc1994.Blazor.IntersectionObserver.API 2 | { 3 | public class IntersectionObserverEntry 4 | { 5 | public bool IsIntersecting { get; set; } 6 | 7 | public double IntersectionRatio { get; set; } 8 | 9 | public DOMRectReadOnly BoundingClientRect { get; set; } 10 | 11 | public DOMRectReadOnly RootBounds { get; set; } 12 | 13 | public bool IsVisible { get; set; } 14 | 15 | public double Time { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/Configuration/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Ljbc1994.Blazor.IntersectionObserver.Configuration 2 | { 3 | internal static class Constants 4 | { 5 | public static string CREATE = "create"; 6 | 7 | public static string OBSERVE = "observe"; 8 | 9 | public static string UNOBSERVE = "unobserve"; 10 | 11 | public static string DISCONNECT = "disconnect"; 12 | 13 | public static string OBSERVE_ELEMENT = "observeElement"; 14 | 15 | public static string REMOVE = "remove"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IIntersectionObserverService.cs: -------------------------------------------------------------------------------- 1 | using Ljbc1994.Blazor.IntersectionObserver.API; 2 | using Microsoft.AspNetCore.Components; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ljbc1994.Blazor.IntersectionObserver 9 | { 10 | public interface IIntersectionObserverService 11 | { 12 | Task Create( 13 | Action> onIntersectUpdate, 14 | IntersectionObserverOptions options = null 15 | ); 16 | 17 | Task Observe( 18 | ElementReference element, 19 | Action> onIntersectUpdate, 20 | IntersectionObserverOptions options = null 21 | ); 22 | 23 | void OnCallback(string id, IList entries); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserve.cs: -------------------------------------------------------------------------------- 1 | using Ljbc1994.Blazor.IntersectionObserver.API; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Components.Rendering; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ljbc1994.Blazor.IntersectionObserver.Components 10 | { 11 | public class IntersectionObserve : ComponentBase, IAsyncDisposable 12 | { 13 | private const string NO_ELEMENT_MESSAGE = "The element reference to observe is required, for example: @ref=\"Context.Ref.Current\" must be provided on the element"; 14 | 15 | [Inject] private IIntersectionObserverService ObserverService { get; set; } 16 | 17 | [Parameter] public RenderFragment ChildContent { get; set; } 18 | 19 | [Parameter] public bool IsIntersecting { get; set; } 20 | 21 | [Parameter] public EventCallback IsIntersectingChanged { get; set; } 22 | 23 | [Parameter] public EventCallback OnChange { get; set; } 24 | 25 | [Parameter] public EventCallback OnDisposed { get; set; } 26 | 27 | [Parameter] public IntersectionObserverOptions Options { get; set; } 28 | 29 | [Parameter] public bool Once { get; set; } 30 | 31 | public IntersectionObserverContext IntersectionObserverContext { get; set; } = new IntersectionObserverContext(); 32 | 33 | private IntersectionObserver Observer { get; set; } 34 | 35 | protected override async Task OnAfterRenderAsync(bool firstRender) 36 | { 37 | if (firstRender) 38 | { 39 | await this.InitialiseObserver(); 40 | } 41 | } 42 | 43 | private async Task InitialiseObserver() 44 | { 45 | var elementReference = this.IntersectionObserverContext?.Ref?.Current; 46 | 47 | if (elementReference == null || Equals(elementReference, default(ElementReference))) 48 | { 49 | throw new Exception(NO_ELEMENT_MESSAGE); 50 | } 51 | 52 | this.Observer = await this.ObserverService.Observe(elementReference.Value, this.OnIntersectUpdate, this.Options); 53 | } 54 | 55 | private async void OnIntersectUpdate(IList entries) 56 | { 57 | var entry = entries?.FirstOrDefault(); 58 | 59 | if (entry == null) return; 60 | 61 | await this.IsIntersectingChanged.InvokeAsync(entry.IsIntersecting); 62 | await this.OnChange.InvokeAsync(entry); 63 | 64 | this.IntersectionObserverContext.Entry = entry; 65 | this.StateHasChanged(); 66 | 67 | if (this.Once && entry.IsIntersecting) 68 | { 69 | await this.Observer.Dispose(); 70 | this.Observer = null; 71 | } 72 | } 73 | 74 | public async ValueTask DisposeAsync() 75 | { 76 | var observer = this.Observer; 77 | 78 | if (observer == null) return; 79 | 80 | this.Observer = null; 81 | await observer.Dispose(); 82 | await this.OnDisposed.InvokeAsync(); 83 | } 84 | 85 | protected override void BuildRenderTree(RenderTreeBuilder builder) 86 | { 87 | if (this.ChildContent == null) 88 | { 89 | throw new Exception($"No element found to observe. {NO_ELEMENT_MESSAGE}"); 90 | } 91 | this.ChildContent(this.IntersectionObserverContext)(builder); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserver.cs: -------------------------------------------------------------------------------- 1 | using Ljbc1994.Blazor.IntersectionObserver.API; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.JSInterop; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ljbc1994.Blazor.IntersectionObserver 10 | { 11 | public class IntersectionObserver 12 | { 13 | /// 14 | /// The identifier for the observer instance 15 | /// 16 | public string Id { get; private set; } 17 | 18 | /// 19 | /// On intersection updates, trigger the action 20 | /// 21 | private event Action> OnIntersectUpdate; 22 | 23 | /// 24 | /// On unobserving an element, trigger the action 25 | /// 26 | private event Func OnObserve; 27 | 28 | /// 29 | /// On unobserving an element, trigger the action 30 | /// 31 | private event Func OnUnobserve; 32 | 33 | /// 34 | /// On disconnecting the observer, trigger the action 35 | /// 36 | private event Func> OnDisconnect; 37 | 38 | /// 39 | /// On disconnecting the observer, trigger the action 40 | /// 41 | private event Func OnRemove; 42 | 43 | /// 44 | /// Initialise the intersection observer with the 45 | /// actions that will be triggered. 46 | /// 47 | /// The observer id 48 | /// On intersection updates 49 | /// Unobserving an element 50 | /// On disconnecting the observer 51 | public IntersectionObserver( 52 | string id, 53 | Action> onIntersectUpdate, 54 | Func onObserve, 55 | Func onUnobserve, 56 | Func> onDisconnect, 57 | Func onRemove 58 | ) 59 | { 60 | this.Id = id; 61 | OnIntersectUpdate = onIntersectUpdate; 62 | OnObserve = onObserve; 63 | OnUnobserve = onUnobserve; 64 | OnDisconnect = onDisconnect; 65 | OnRemove = onRemove; 66 | } 67 | 68 | /// 69 | /// On intersection, pass the entries 70 | /// to the actions(s). 71 | /// 72 | /// 73 | public void OnIntersect(IList entries) 74 | { 75 | if (entries != null && entries.Any()) 76 | { 77 | OnIntersectUpdate?.Invoke(entries); 78 | } 79 | } 80 | 81 | /// 82 | /// On observing an element, pass the element 83 | /// reference to the action(s). 84 | /// 85 | /// The element to observe 86 | public async ValueTask Observe(ElementReference elementRef) 87 | { 88 | await (OnObserve.Invoke(this.Id, elementRef)); 89 | } 90 | 91 | /// 92 | /// On unobserving an element, pass the element 93 | /// reference to the action(s). 94 | /// 95 | /// The element to unobserve 96 | public async ValueTask Unobserve(ElementReference elementRef) 97 | { 98 | await (OnUnobserve.Invoke(this.Id, elementRef)); 99 | } 100 | 101 | /// 102 | /// On disconnecting the observer, trigger 103 | /// the action(s). 104 | /// 105 | public async ValueTask Disconnect() 106 | { 107 | await (OnDisconnect.Invoke(this.Id)); 108 | } 109 | 110 | /// 111 | /// Signal that the observer should be 112 | /// disposed. 113 | /// 114 | public async ValueTask Dispose() 115 | { 116 | await OnRemove.Invoke(this.Id); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserverContext.cs: -------------------------------------------------------------------------------- 1 | using Ljbc1994.Blazor.IntersectionObserver.API; 2 | 3 | namespace Ljbc1994.Blazor.IntersectionObserver 4 | { 5 | public class IntersectionObserverContext 6 | { 7 | public IntersectionObserverEntry Entry { get; set; } 8 | public ForwardReference Ref { get; set; } = new ForwardReference(); 9 | public bool IsIntersecting => this.Entry?.IsIntersecting ?? false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserverExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Ljbc1994.Blazor.IntersectionObserver 4 | { 5 | public static class IntersectionObserverExtensions 6 | { 7 | public static IServiceCollection AddIntersectionObserver(this IServiceCollection services) 8 | { 9 | return services.AddScoped(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserverOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System.Collections.Generic; 4 | 5 | namespace Ljbc1994.Blazor.IntersectionObserver 6 | { 7 | public class IntersectionObserverOptions 8 | { 9 | public ElementReference? Root { get; set; } 10 | 11 | public IList Threshold { get; set; } = new List { 1.0 }; 12 | 13 | public string RootMargin { get; set; } = "0px"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/IntersectionObserverService.cs: -------------------------------------------------------------------------------- 1 | using Ljbc1994.Blazor.IntersectionObserver.API; 2 | using Ljbc1994.Blazor.IntersectionObserver.Configuration; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | 10 | namespace Ljbc1994.Blazor.IntersectionObserver 11 | { 12 | public class IntersectionObserverService: IIntersectionObserverService, IAsyncDisposable 13 | { 14 | private readonly string scriptPath = "_content/BlazorIntersectionObserver/blazor-intersection-observer.min.js"; 15 | 16 | private readonly Task moduleTask; 17 | 18 | private DotNetObjectReference objectRef; 19 | 20 | /// 21 | /// Contains a reference of observer instances and their ids. 22 | /// 23 | private readonly ConcurrentDictionary observers = new ConcurrentDictionary(); 24 | 25 | public IntersectionObserverService(IJSRuntime jsRuntime, NavigationManager navManager) 26 | { 27 | var path = navManager.ToAbsoluteUri(this.scriptPath); 28 | this.moduleTask = jsRuntime.InvokeAsync("import", path).AsTask(); 29 | this.objectRef = DotNetObjectReference.Create(this); 30 | } 31 | 32 | /// 33 | /// Create an observer instance with a unique id, adding it 34 | /// to the list of observers. 35 | /// 36 | /// On an intersection update, pass the entries to the callback 37 | /// The intersection observer options 38 | /// The observer instance 39 | public async Task Create( 40 | Action> onIntersectUpdate, 41 | IntersectionObserverOptions options = null 42 | ) 43 | { 44 | var module = await this.moduleTask; 45 | 46 | var callbackId = Guid.NewGuid().ToString(); 47 | 48 | await module.InvokeAsync(Constants.CREATE, this.objectRef, callbackId, options); 49 | 50 | return this.CreateObserver(callbackId, onIntersectUpdate); 51 | } 52 | 53 | /// 54 | /// Create an observer instance and immediately observe 55 | /// the element. 56 | /// 57 | /// The element 58 | /// On an intersection update, pass the entries 59 | /// The intersection observer options 60 | /// 61 | public async Task Observe( 62 | ElementReference element, 63 | Action> onIntersectUpdate, 64 | IntersectionObserverOptions options = null 65 | ) 66 | { 67 | var module = await this.moduleTask; 68 | 69 | var callbackId = Guid.NewGuid().ToString(); 70 | 71 | await module.InvokeAsync(Constants.OBSERVE, this.objectRef, callbackId, element, options); 72 | 73 | return this.CreateObserver(callbackId, onIntersectUpdate); 74 | } 75 | 76 | /// 77 | /// This is triggered in js when an intersection update 78 | /// has occurred. 79 | /// 80 | /// The observer id 81 | /// The intersection observer entries 82 | [JSInvokable(nameof(OnCallback))] 83 | public void OnCallback(string id, IList entries) 84 | { 85 | this.EnsureObserverExists(id); 86 | 87 | if (this.observers.TryGetValue(id, out var observer)) 88 | { 89 | observer.OnIntersect(entries); 90 | } 91 | } 92 | 93 | /// 94 | /// Create an observer, passing the service callbacks to trigger. 95 | /// 96 | /// The observer instance id 97 | /// On intersection update, pass the entries 98 | /// The observer instance 99 | private IntersectionObserver CreateObserver(string observerId, Action> onIntersectUpdate) 100 | { 101 | var observer = new IntersectionObserver( 102 | observerId, 103 | onIntersectUpdate, 104 | this.ObserveElement, 105 | this.Unobserve, 106 | this.Disconnect, 107 | this.RemoveObserver 108 | ); 109 | 110 | if (this.observers.TryAdd(observerId, observer)) 111 | { 112 | return observer; 113 | } 114 | 115 | throw new Exception($"Failed to create observer for key: {observerId}"); 116 | } 117 | 118 | /// 119 | /// Observe an element using an observer instance 120 | /// 121 | /// The observer instance id 122 | /// The element to observe 123 | private async ValueTask ObserveElement(string id, ElementReference element) 124 | { 125 | if (this.observers.ContainsKey(id)) 126 | { 127 | var module = await this.moduleTask; 128 | 129 | await module.InvokeAsync(Constants.OBSERVE_ELEMENT, id, element); 130 | } 131 | } 132 | 133 | /// 134 | /// Unobserve an element on an observer instance 135 | /// 136 | /// The observer instance id 137 | /// The element to unobserve 138 | private async ValueTask Unobserve(string id, ElementReference element) 139 | { 140 | var module = await this.moduleTask; 141 | 142 | await module.InvokeAsync(Constants.UNOBSERVE, id, element); 143 | } 144 | 145 | /// 146 | /// Disconnect the observer instance 147 | /// 148 | /// The observer instance id 149 | private async ValueTask Disconnect(string id) 150 | { 151 | var module = await this.moduleTask; 152 | 153 | return await module.InvokeAsync(Constants.DISCONNECT, id); 154 | } 155 | 156 | /// 157 | /// Disconnect the observer instance 158 | /// 159 | /// The observer instance id 160 | private async ValueTask RemoveObserver(string id) 161 | { 162 | var module = await this.moduleTask; 163 | 164 | var disposed = await module.InvokeAsync(Constants.REMOVE, id); 165 | 166 | if (disposed) 167 | { 168 | this.observers.TryRemove(id, out _); 169 | } 170 | } 171 | 172 | private void EnsureObserverExists(string id) 173 | { 174 | if (!this.observers.ContainsKey(id)) 175 | { 176 | throw new Exception($"There is no observer for key: {id}"); 177 | } 178 | } 179 | 180 | public async ValueTask DisposeAsync() 181 | { 182 | this.objectRef?.Dispose(); 183 | 184 | await Task.CompletedTask; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/Ljbc1994.Blazor.IntersectionObserver.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | SAK 4 | SAK 5 | SAK 6 | SAK 7 | 8 | 9 | 10 | BlazorIntersectionObserver 11 | net5.0 12 | 8.0 13 | 3.0 14 | true 15 | true 16 | Blazor Intersection Observer 17 | Intersection Observer API for Blazor applications 18 | true 19 | 3.1.0 20 | BlazorIntersectionObserver 21 | README.md 22 | 23 | 3.1.0 24 | - Use the navigation manager for importing intersection observer script 25 | 26 | 3.0.1 27 | - Add helpful error messages if the consumer fails to provide an element to observe or does not provide 28 | any child content. 29 | 30 | 3.0.0 31 | - *BREAKING CHANGE* Namespace has been changed to `Ljbc1994.Blazor.IntersectionObserver` to avoid 32 | namespace conflicts with Blazor libraries. 33 | 34 | 2.0.1 35 | - *BREAKING CHANGE* The IntersectionObserve component now requires a reference to the node it's observing. 36 | - The imported observer script is now minified. 37 | 38 | 1.0.0 39 | - Updated project to use dotnet 5 40 | LICENCE.txt 41 | Copyright © 2021 - Louie Colgan 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | full 50 | 4 51 | true 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Always 93 | 94 | 95 | Always 96 | 97 | 98 | 99 | 100 | 101 | True 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:51056/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Blazor.IntersectionObserver": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:51057/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/__tests__/data/index.ts: -------------------------------------------------------------------------------- 1 | const dummyRect: ClientRect | DOMRect = { 2 | bottom: 0, 3 | top: 10, 4 | height: 20, 5 | left: 30, 6 | right: 20, 7 | width: 100, 8 | x: 20, 9 | y: 32 10 | }; 11 | 12 | export function getClientOrDomRect(rect?: Partial): any { 13 | return { 14 | ...dummyRect, 15 | ...rect 16 | }; 17 | } 18 | 19 | const dummyObserverEntry: IntersectionObserverEntry = { 20 | boundingClientRect: getClientOrDomRect(), 21 | intersectionRect: getClientOrDomRect(), 22 | rootBounds: getClientOrDomRect(), 23 | intersectionRatio: 1, 24 | isIntersecting: true, 25 | target: document.createElement("div"), 26 | time: 12 27 | }; 28 | 29 | export function getObserverEntry(entry?: Partial): IntersectionObserverEntry { 30 | return { 31 | ...dummyObserverEntry, 32 | ...entry 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as ObserverJS from "../src/index"; 2 | import { getObserverEntry } from "./data/index"; 3 | 4 | declare var window: any; 5 | 6 | const observe = jest.fn(); 7 | const unobserve = jest.fn(); 8 | const disconnect = jest.fn(); 9 | let onEntryChangeCallback: ObserverJS.OnIntersectionUpdateFn | null; 10 | let observerOptions: IntersectionObserverInit | null; 11 | 12 | const mockDotNetRef = { 13 | invokeMethodAsync: jest.fn(), 14 | }; 15 | 16 | window.IntersectionObserver = jest.fn(function (fn, options) { 17 | onEntryChangeCallback = fn; 18 | observerOptions = options; 19 | return { 20 | observe, 21 | unobserve, 22 | disconnect, 23 | }; 24 | }); 25 | 26 | beforeEach(() => { 27 | ObserverJS.reset(); 28 | observe.mockReset(); 29 | unobserve.mockReset(); 30 | disconnect.mockReset(); 31 | mockDotNetRef.invokeMethodAsync.mockReset(); 32 | onEntryChangeCallback = null; 33 | observerOptions = null; 34 | }); 35 | 36 | describe("when creating an observer", () => { 37 | it("should create a new observer item", () => { 38 | const callbackId = "1"; 39 | 40 | ObserverJS.create(mockDotNetRef, callbackId); 41 | 42 | const items = ObserverJS.getObserverItems(); 43 | 44 | expect(items.size).toBe(1); 45 | }); 46 | 47 | it("should create multiple observer items", () => { 48 | const callbackIds = ["1", "2"]; 49 | 50 | callbackIds.forEach((id) => ObserverJS.create(mockDotNetRef, id)); 51 | 52 | const items = ObserverJS.getObserverItems(); 53 | 54 | expect(items.size).toBe(2); 55 | }); 56 | 57 | it("should create an observer item and observe elements", () => { 58 | const callbackId = "1"; 59 | 60 | ObserverJS.create(mockDotNetRef, callbackId); 61 | 62 | const mockDiv = document.createElement("div"); 63 | const mockSpan = document.createElement("span"); 64 | 65 | const observeElementId1 = ObserverJS.observeElement(callbackId, mockDiv); 66 | const observeElementId2 = ObserverJS.observeElement(callbackId, mockSpan); 67 | 68 | expect(observe).toBeCalledTimes(2); 69 | expect(observeElementId1).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); 70 | expect(observeElementId2).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}1`); 71 | }); 72 | }); 73 | 74 | describe("when observing an element", () => { 75 | it("should create an observer item and immediately observe the element", () => { 76 | const callbackId = "1"; 77 | const mockDiv = document.createElement("div"); 78 | 79 | const observeElementId = ObserverJS.observe( 80 | mockDotNetRef, 81 | callbackId, 82 | mockDiv 83 | ); 84 | 85 | const items = ObserverJS.getObserverItems(); 86 | 87 | expect(items.size).toBe(1); 88 | expect(observe).toBeCalledTimes(1); 89 | expect(observeElementId).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); 90 | }); 91 | 92 | it("should observe the element within a threshold", () => { 93 | const callbackId = "1"; 94 | const mockDiv = document.createElement("div"); 95 | const definedOptions = { threshold: 0.5 }; 96 | 97 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv, definedOptions); 98 | 99 | const items = ObserverJS.getObserverItems(); 100 | 101 | expect(items.size).toBe(1); 102 | expect(observe).toBeCalledTimes(1); 103 | expect(observerOptions).toBe(definedOptions); 104 | }); 105 | 106 | it("should throw an error if the observer item does not exist", () => { 107 | const callbackId = "1"; 108 | const mockDiv = document.createElement("div"); 109 | 110 | expect(() => { 111 | ObserverJS.observeElement(callbackId, mockDiv); 112 | }).toThrowError(); 113 | }); 114 | }); 115 | 116 | describe("when unobserving an element", () => { 117 | it("should unobserve an element for an observer item", () => { 118 | const callbackId = "1"; 119 | const mockDiv = document.createElement("div"); 120 | 121 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); 122 | const removedElementId = ObserverJS.unobserve(callbackId, mockDiv); 123 | 124 | const items = ObserverJS.getObserverItems(); 125 | const instance = items.get(callbackId); 126 | 127 | expect(unobserve).toHaveBeenCalledTimes(1); 128 | expect(items.size).toBe(1); 129 | expect(instance).toBeDefined(); 130 | expect(removedElementId).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); 131 | }); 132 | 133 | it("should throw an error if the observer item does not exist", () => { 134 | const callbackId = "1"; 135 | const mockDiv = document.createElement("div"); 136 | 137 | expect(() => { 138 | ObserverJS.unobserve(callbackId, mockDiv); 139 | }).toThrowError(); 140 | }); 141 | }); 142 | 143 | describe("when disconnecting an observer", () => { 144 | it("should disconnect an observer for observer item", () => { 145 | const callbackId = "1"; 146 | const mockDiv = document.createElement("div"); 147 | 148 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); 149 | ObserverJS.disconnect(callbackId); 150 | 151 | expect(disconnect).toHaveBeenCalledTimes(1); 152 | }); 153 | 154 | it("should throw an error if the observer item does not exist", () => { 155 | const callbackId = "1"; 156 | 157 | expect(() => { 158 | ObserverJS.disconnect(callbackId); 159 | }).toThrowError(); 160 | }); 161 | }); 162 | 163 | describe("when removing an observer item", () => { 164 | it("should remove the observer item from the observer items", () => { 165 | const callbackId = "1"; 166 | const mockDiv = document.createElement("div"); 167 | 168 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); 169 | ObserverJS.remove(callbackId); 170 | 171 | const items = ObserverJS.getObserverItems(); 172 | 173 | expect(disconnect).toHaveBeenCalledTimes(1); 174 | expect(items.size).toBe(0); 175 | }); 176 | 177 | it("should throw an error if the observer item does not exist", () => { 178 | const callbackId = "1"; 179 | 180 | expect(() => { 181 | ObserverJS.remove(callbackId); 182 | }).toThrowError(); 183 | }); 184 | }); 185 | 186 | describe("when an element is intersecting", () => { 187 | it("should return a list of entries to the observer instance dotnet reference", () => { 188 | const callbackId = "1"; 189 | const mockDiv = document.createElement("div"); 190 | 191 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); 192 | 193 | const entry = getObserverEntry({ target: mockDiv }); 194 | 195 | onEntryChangeCallback!([entry]); 196 | 197 | const [[arg1, arg2, [passedEntry]]] = 198 | mockDotNetRef.invokeMethodAsync.mock.calls; 199 | 200 | expect(arg1).toBe("OnCallback"); 201 | expect(arg2).toBe(callbackId); 202 | }); 203 | 204 | it("should not return a list of entries if the observer item does not exist", () => { 205 | const callbackId = "1"; 206 | const mockDiv = document.createElement("div"); 207 | 208 | ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); 209 | ObserverJS.remove(callbackId); 210 | 211 | onEntryChangeCallback!([getObserverEntry()]); 212 | 213 | expect(mockDotNetRef.invokeMethodAsync).toHaveBeenCalledTimes(0); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/__tests__/utils/config.ts: -------------------------------------------------------------------------------- 1 | import * as ObserverJS from "../../src/index"; 2 | 3 | export function getObserverItemId(id: string) { 4 | return `${ObserverJS.OBSERVER_ID_PREFIX}${id}`; 5 | } -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/__tests__/utils/document.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getMockElement(tagName: string) { 3 | return document.createElement(tagName); 4 | } 5 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/__tests__/utils/iterable.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getSetValueFirstOrDefault(map: Map): Y { 3 | return map.values().next().value; 4 | } -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "" 4 | ], 5 | "transform": { 6 | "^.+\\.ts?$": "ts-jest" 7 | }, 8 | "testRegex": "(/__tests__/.*|(\\.|/)).(test|spec).(jsx?|ts?)$", 9 | "coverageReporters": [ 10 | "cobertura" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blazor.intersectionobserver", 3 | "private": true, 4 | "version": "1.1.0", 5 | "description": "Intersection Observer wrapper for Blazor", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "build": "rollup -c", 9 | "test": "jest --ci --reporters=default --reporters=jest-junit --coverage --coverageReporters cobertura", 10 | "test:watch": "npm test -- --watch" 11 | }, 12 | "author": "Louie Colgan <@ljbc1994>", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@rollup/plugin-typescript": "8.0.0", 16 | "@types/jest": "26.0.15", 17 | "jest": "26.6.3", 18 | "jest-junit": "12.0.0", 19 | "rollup": "^2.34.0", 20 | "rollup-plugin-terser": "^7.0.2", 21 | "ts-jest": "26.4.4", 22 | "tslib": "^2.0.3", 23 | "typescript": "4.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { terser } from "rollup-plugin-terser"; 3 | 4 | export default [ 5 | { 6 | input: 'src/index.ts', 7 | output: { 8 | file: 'wwwroot/blazor-intersection-observer.min.js', 9 | format: 'es', 10 | sourcemap: false, 11 | }, 12 | plugins: [typescript({ 13 | sourceMap: false, 14 | }), terser()], 15 | }, 16 | { 17 | input: 'src/index.ts', 18 | output: { 19 | file: 'wwwroot/blazor-intersection-observer.js', 20 | format: 'es', 21 | sourcemap: false, 22 | }, 23 | plugins: [typescript({ 24 | sourceMap: false, 25 | })], 26 | } 27 | ]; 28 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/src/index.ts: -------------------------------------------------------------------------------- 1 | export interface DotNetObjectRef { 2 | invokeMethodAsync(methodName: string, ...args: any[]): Promise; 3 | } 4 | 5 | export interface IntersectionObserverItemElement { 6 | element: Element; 7 | elementId: string; 8 | } 9 | 10 | export interface IntersectionObserverItem { 11 | dotnetRef: DotNetObjectRef; 12 | observer: IntersectionObserver; 13 | elements: IntersectionObserverItemElement[]; 14 | } 15 | 16 | export type OnIntersectionUpdateFn = ( 17 | entries: IntersectionObserverEntry[] 18 | ) => void; 19 | 20 | export const OBSERVER_ID_PREFIX = "blazor_plugin_observer__"; 21 | 22 | let OBSERVER_ID = 1; 23 | 24 | const observerItems = new Map(); 25 | 26 | /** 27 | * Reset the counter and the observer instances 28 | */ 29 | export function reset() { 30 | OBSERVER_ID = 0; 31 | observerItems.clear(); 32 | } 33 | 34 | /** 35 | * Get the observer items 36 | */ 37 | export function getObserverItems() { 38 | return observerItems; 39 | } 40 | 41 | /** 42 | * Generate a unique id for an observer item. 43 | **/ 44 | function getObserverElementId() { 45 | return `${OBSERVER_ID_PREFIX}${OBSERVER_ID++}`; 46 | } 47 | 48 | /** 49 | * Create a new intersection observer item. 50 | * @param {DotNetObjectRef} dotnetRef - The current dotnet blazor reference 51 | * @param {string} callbackId - The callback id for the blazor observer service 52 | * @param {globalThis.IntersectionObserverInit} options - The intersection options 53 | * @returns {IntersectionObserverItem} - The intersection observer item 54 | */ 55 | function createObserverItem( 56 | dotnetRef: DotNetObjectRef, 57 | callbackId: string, 58 | options?: globalThis.IntersectionObserverInit 59 | ): IntersectionObserverItem { 60 | const onEntry = onEntryChange(callbackId); 61 | 62 | const observer = new IntersectionObserver(onEntry, options); 63 | 64 | observerItems.set(callbackId, { dotnetRef, observer, elements: [] }); 65 | 66 | return observerItems.get(callbackId)!; 67 | } 68 | 69 | /** 70 | * Observe an element for the observer item 71 | * @param {string} callbackId - The callback id for the blazor observer service 72 | * @param {Element} element - The element to observe 73 | * @returns {string} - The observer element id 74 | */ 75 | export function observeElement(callbackId: string, element: Element): string { 76 | const item = observerItems.get(callbackId); 77 | 78 | if (item == null) { 79 | throw new Error(`Failed to observe element for key: ${callbackId} as the observer does not exist`); 80 | } 81 | 82 | if (item.elements.some(record => record.element == element)) { 83 | console.warn(`BlazorIntersectionObserver: The element is already being observed by observer for key ${callbackId}`); 84 | return ""; 85 | } 86 | 87 | const elementId = getObserverElementId(); 88 | 89 | item.observer.observe(element); 90 | item.elements.push({ elementId, element }); 91 | 92 | return elementId; 93 | } 94 | 95 | /** 96 | * Create a intersection observer. 97 | * @param {IDotNetObjectRef} dotnetRef - The dotnet interop reference 98 | * @param {string} callbackId - The callback id for the blazor observer service 99 | * @param {globalThis.IntersectionObserverInit} options - The intersection observer options 100 | * @returns {IntersectionObserverItem} - The observer item 101 | */ 102 | export function create( 103 | dotnetRef: DotNetObjectRef, 104 | callbackId: string, 105 | options?: globalThis.IntersectionObserverInit 106 | ) { 107 | return createObserverItem(dotnetRef, callbackId, options); 108 | } 109 | 110 | /** 111 | * Observe the target node using a new observer 112 | * @param {DotNetObjectRef} dotnetRef - The dotnet interop reference 113 | * @param {string} callbackId - The callback id for the blazor observer service 114 | * @param {Element} node - The node to observe 115 | * @param {globalThis.IntersectionObserverInit} options - The intersection observer options 116 | * @returns {string} - The observer element id 117 | */ 118 | export function observe( 119 | dotnetRef: DotNetObjectRef, 120 | callbackId: string, 121 | node: Element, 122 | options?: globalThis.IntersectionObserverInit 123 | ): string { 124 | createObserverItem(dotnetRef, callbackId, options); 125 | return observeElement(callbackId, node); 126 | } 127 | 128 | /** 129 | * Unobserve the element for the observer item. 130 | * @param {string} id - The observer item id 131 | * @param {Element} element - The element to unobserve 132 | * @returns {boolean} - Whether the element has been unobserved 133 | */ 134 | export function unobserve(callbackId: string, element: Element): string { 135 | const item = observerItems.get(callbackId); 136 | 137 | if (item == null) { 138 | throw new Error(`Failed to unobserve element for key: ${callbackId} as the observer does not exist`); 139 | } 140 | 141 | const unobserveElementId = item.elements.find((record) => record.element == element)?.elementId; 142 | 143 | if (unobserveElementId == null) { 144 | console.warn(`BlazorIntersectionObserver: The record does not exist for observer: ${callbackId}`); 145 | } 146 | 147 | item.observer.unobserve(element); 148 | item.elements = item.elements.filter((record) => record.element != element); 149 | 150 | return unobserveElementId!; 151 | } 152 | 153 | /** 154 | * Disconnect the observered elements from the observer item. 155 | * @param {string} callbackId - The observer item id 156 | * @returns {boolean} - Whether the elements have 157 | * been removed from the observer item 158 | */ 159 | export function disconnect(callbackId: string): boolean { 160 | const item = observerItems.get(callbackId); 161 | 162 | if (item == null) { 163 | throw new Error(`Failed to disconnect for key: ${callbackId} as the observer does not exist`); 164 | } 165 | 166 | item.observer.disconnect(); 167 | item.elements = []; 168 | 169 | return true; 170 | } 171 | 172 | /** 173 | * Remove the observer item. 174 | * @param {string} callbackId - The observer item id 175 | * @returns {boolean} - Whether the observer item has been 176 | * removed. 177 | */ 178 | export function remove(callbackId: string): boolean { 179 | if (disconnect(callbackId)) { 180 | return observerItems.delete(callbackId); 181 | } 182 | return false; 183 | } 184 | 185 | /** 186 | * Convert the observer entry to an object that 187 | * will be parsed to the callback. 188 | * @param {IntersectionObserverEntry} entry - The observer entry 189 | */ 190 | function toEntryObject(entry: IntersectionObserverEntry) { 191 | function toRectReadOnlyObject(obj) { 192 | if (!obj) return null; 193 | return { 194 | X: obj.x, 195 | Y: obj.y, 196 | Width: obj.width, 197 | Height: obj.height, 198 | Top: obj.top, 199 | Left: obj.left, 200 | Bottom: obj.bottom, 201 | Right: obj.right, 202 | }; 203 | } 204 | 205 | return { 206 | IsIntersecting: entry.isIntersecting, 207 | IntersectionRatio: entry.intersectionRatio, 208 | Time: entry.time, 209 | BoundingClientRect: toRectReadOnlyObject(entry.boundingClientRect), 210 | IntersectionRect: toRectReadOnlyObject(entry.intersectionRect), 211 | RootBounds: toRectReadOnlyObject(entry.rootBounds), 212 | }; 213 | } 214 | 215 | /** 216 | * Returns a function that will be triggered when an 217 | * element has an intersection update. 218 | * @param {string} callbackId - The observer item id 219 | * @returns {OnIntersectionUpdateFn} - The function triggered by an intersection update 220 | */ 221 | function onEntryChange(callbackId: string): OnIntersectionUpdateFn { 222 | return (entries: readonly IntersectionObserverEntry[]) => { 223 | 224 | if (!observerItems.has(callbackId)) { 225 | return; 226 | } 227 | 228 | const { dotnetRef } = observerItems.get(callbackId)!; 229 | 230 | const mapped = entries.map((entry) => { 231 | const mappedEntry = toEntryObject(entry); 232 | return mappedEntry; 233 | }); 234 | 235 | dotnetRef.invokeMethodAsync( 236 | "OnCallback", 237 | callbackId, 238 | mapped 239 | ); 240 | }; 241 | } 242 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noEmitOnError": true, 5 | "removeComments": true, 6 | "sourceMap": true, 7 | "target": "ES2019", 8 | "module": "esnext", 9 | "lib": [ "ES2019", "dom" ], 10 | "strict": true, 11 | "alwaysStrict": true, 12 | "preserveConstEnums": true, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node" 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.js: -------------------------------------------------------------------------------- 1 | const OBSERVER_ID_PREFIX = "blazor_plugin_observer__"; 2 | let OBSERVER_ID = 1; 3 | const observerItems = new Map(); 4 | function reset() { 5 | OBSERVER_ID = 0; 6 | observerItems.clear(); 7 | } 8 | function getObserverItems() { 9 | return observerItems; 10 | } 11 | function getObserverElementId() { 12 | return `${OBSERVER_ID_PREFIX}${OBSERVER_ID++}`; 13 | } 14 | function createObserverItem(dotnetRef, callbackId, options) { 15 | const onEntry = onEntryChange(callbackId); 16 | const observer = new IntersectionObserver(onEntry, options); 17 | observerItems.set(callbackId, { dotnetRef, observer, elements: [] }); 18 | return observerItems.get(callbackId); 19 | } 20 | function observeElement(callbackId, element) { 21 | const item = observerItems.get(callbackId); 22 | if (item == null) { 23 | throw new Error(`Failed to observe element for key: ${callbackId} as the observer does not exist`); 24 | } 25 | if (item.elements.some(record => record.element == element)) { 26 | console.warn(`BlazorIntersectionObserver: The element is already being observed by observer for key ${callbackId}`); 27 | return ""; 28 | } 29 | const elementId = getObserverElementId(); 30 | item.observer.observe(element); 31 | item.elements.push({ elementId, element }); 32 | return elementId; 33 | } 34 | function create(dotnetRef, callbackId, options) { 35 | return createObserverItem(dotnetRef, callbackId, options); 36 | } 37 | function observe(dotnetRef, callbackId, node, options) { 38 | createObserverItem(dotnetRef, callbackId, options); 39 | return observeElement(callbackId, node); 40 | } 41 | function unobserve(callbackId, element) { 42 | var _a; 43 | const item = observerItems.get(callbackId); 44 | if (item == null) { 45 | throw new Error(`Failed to unobserve element for key: ${callbackId} as the observer does not exist`); 46 | } 47 | const unobserveElementId = (_a = item.elements.find((record) => record.element == element)) === null || _a === void 0 ? void 0 : _a.elementId; 48 | if (unobserveElementId == null) { 49 | console.warn(`BlazorIntersectionObserver: The record does not exist for observer: ${callbackId}`); 50 | } 51 | item.observer.unobserve(element); 52 | item.elements = item.elements.filter((record) => record.element != element); 53 | return unobserveElementId; 54 | } 55 | function disconnect(callbackId) { 56 | const item = observerItems.get(callbackId); 57 | if (item == null) { 58 | throw new Error(`Failed to disconnect for key: ${callbackId} as the observer does not exist`); 59 | } 60 | item.observer.disconnect(); 61 | item.elements = []; 62 | return true; 63 | } 64 | function remove(callbackId) { 65 | if (disconnect(callbackId)) { 66 | return observerItems.delete(callbackId); 67 | } 68 | } 69 | function toEntryObject(entry) { 70 | function toRectReadOnlyObject(obj) { 71 | if (!obj) 72 | return null; 73 | return { 74 | X: obj.x, 75 | Y: obj.y, 76 | Width: obj.width, 77 | Height: obj.height, 78 | Top: obj.top, 79 | Left: obj.left, 80 | Bottom: obj.bottom, 81 | Right: obj.right, 82 | }; 83 | } 84 | return { 85 | IsIntersecting: entry.isIntersecting, 86 | IntersectionRatio: entry.intersectionRatio, 87 | Time: entry.time, 88 | BoundingClientRect: toRectReadOnlyObject(entry.boundingClientRect), 89 | IntersectionRect: toRectReadOnlyObject(entry.intersectionRect), 90 | RootBounds: toRectReadOnlyObject(entry.rootBounds), 91 | }; 92 | } 93 | function onEntryChange(callbackId) { 94 | return (entries) => { 95 | if (!observerItems.has(callbackId)) { 96 | return; 97 | } 98 | const { dotnetRef } = observerItems.get(callbackId); 99 | const mapped = entries.map((entry) => { 100 | const mappedEntry = toEntryObject(entry); 101 | return mappedEntry; 102 | }); 103 | dotnetRef.invokeMethodAsync("OnCallback", callbackId, mapped); 104 | }; 105 | } 106 | 107 | export { OBSERVER_ID_PREFIX, create, disconnect, getObserverItems, observe, observeElement, remove, reset, unobserve }; 108 | -------------------------------------------------------------------------------- /src/Ljbc1994.Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.min.js: -------------------------------------------------------------------------------- 1 | const e="blazor_plugin_observer__";let t=1;const n=new Map;function o(){t=0,n.clear()}function r(){return n}function s(e,t,o){const r=function(e){return t=>{if(!n.has(e))return;const{dotnetRef:o}=n.get(e),r=t.map((e=>{const t=function(e){function t(e){return e?{X:e.x,Y:e.y,Width:e.width,Height:e.height,Top:e.top,Left:e.left,Bottom:e.bottom,Right:e.right}:null}return{IsIntersecting:e.isIntersecting,IntersectionRatio:e.intersectionRatio,Time:e.time,BoundingClientRect:t(e.boundingClientRect),IntersectionRect:t(e.intersectionRect),RootBounds:t(e.rootBounds)}}(e);return t}));o.invokeMethodAsync("OnCallback",e,r)}}(t),s=new IntersectionObserver(r,o);return n.set(t,{dotnetRef:e,observer:s,elements:[]}),n.get(t)}function i(e,o){const r=n.get(e);if(null==r)throw new Error(`Failed to observe element for key: ${e} as the observer does not exist`);if(r.elements.some((e=>e.element==o)))return console.warn(`BlazorIntersectionObserver: The element is already being observed by observer for key ${e}`),"";const s="blazor_plugin_observer__"+t++;return r.observer.observe(o),r.elements.push({elementId:s,element:o}),s}function l(e,t,n){return s(e,t,n)}function c(e,t,n,o){return s(e,t,o),i(t,n)}function u(e,t){var o;const r=n.get(e);if(null==r)throw new Error(`Failed to unobserve element for key: ${e} as the observer does not exist`);const s=null===(o=r.elements.find((e=>e.element==t)))||void 0===o?void 0:o.elementId;return null==s&&console.warn(`BlazorIntersectionObserver: The record does not exist for observer: ${e}`),r.observer.unobserve(t),r.elements=r.elements.filter((e=>e.element!=t)),s}function f(e){const t=n.get(e);if(null==t)throw new Error(`Failed to disconnect for key: ${e} as the observer does not exist`);return t.observer.disconnect(),t.elements=[],!0}function d(e){if(f(e))return n.delete(e)}export{e as OBSERVER_ID_PREFIX,l as create,f as disconnect,r as getObserverItems,c as observe,i as observeElement,d as remove,o as reset,u as unobserve}; 2 | --------------------------------------------------------------------------------