receptive 0.1.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +43 -0
- data/HANDBOOK.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +22 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib-opal/receptive.js.rb +13 -0
- data/lib-opal/receptive/app.rb +48 -0
- data/lib-opal/receptive/incremental-dom-0.5.1.js +1223 -0
- data/lib-opal/receptive/incremental_dom.js.rb +87 -0
- data/lib-opal/receptive/view.rb +95 -0
- data/lib/receptive.rb +14 -0
- data/lib/receptive/version.rb +3 -0
- data/receptive.gemspec +31 -0
- data/test-opal/receptive_test.rb +12 -0
- data/test-opal/receptive_view_test.rb +13 -0
- data/test-opal/test_helper.rb +8 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9f4b45907709281428dd8d3864bed092143f4e7542c0f1a5b9d9aed2c005b95f
|
4
|
+
data.tar.gz: 22a456c8dd79e115ba8cbe2fb3706e75b4f70b31b310ae6412067405616fe34a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b543eb6ee8a093fd62a9b3dce5db88418b1cdfeb102e1a0a461e14cfd9284d74bb2b91c1efeda359f4bcc49def4f0d5bbe137988bedaa1864109387def270dea
|
7
|
+
data.tar.gz: d11818c1327dd29fb395304bd299ac1b4cc1d981e0cb3b03ea32d80108de0c82d382cd7fa73722c3ef1cbb4f1595e77c5ef15b18103e401464dedf9ef8cb7075
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
receptive (0.1.0.alpha)
|
5
|
+
opal (>= 0.10.5, < 0.12)
|
6
|
+
opal-jquery (~> 0.4.2)
|
7
|
+
opal-minitest (= 0.0.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
concurrent-ruby (1.0.5)
|
13
|
+
hike (1.2.3)
|
14
|
+
minitest (5.11.3)
|
15
|
+
opal (0.10.5)
|
16
|
+
hike (~> 1.2)
|
17
|
+
sourcemap (~> 0.1.0)
|
18
|
+
sprockets (~> 3.1)
|
19
|
+
tilt (>= 1.4)
|
20
|
+
opal-jquery (0.4.2)
|
21
|
+
opal (>= 0.7.0, < 0.11.0)
|
22
|
+
opal-minitest (0.0.5)
|
23
|
+
opal (>= 0.8)
|
24
|
+
rake (~> 10)
|
25
|
+
rack (2.0.4)
|
26
|
+
rake (10.5.0)
|
27
|
+
sourcemap (0.1.1)
|
28
|
+
sprockets (3.7.1)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
rack (> 1, < 3)
|
31
|
+
tilt (2.0.8)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
bundler (~> 1.16)
|
38
|
+
minitest (~> 5.0)
|
39
|
+
rake (~> 10.0)
|
40
|
+
receptive!
|
41
|
+
|
42
|
+
BUNDLED WITH
|
43
|
+
1.16.1
|
data/HANDBOOK.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Receptive handbook
|
2
|
+
|
3
|
+
## A basic view
|
4
|
+
|
5
|
+
The README example, annotated:
|
6
|
+
|
7
|
+
```html
|
8
|
+
<div class="hello-world">
|
9
|
+
<input type="text">
|
10
|
+
<button>Greet</button>
|
11
|
+
<span class="output"></span>
|
12
|
+
</div>
|
13
|
+
```
|
14
|
+
|
15
|
+
```rb
|
16
|
+
require 'opal'
|
17
|
+
require 'receptive'
|
18
|
+
|
19
|
+
class HelloWorld
|
20
|
+
extend Receptive::View
|
21
|
+
|
22
|
+
self.selector = ".hello-world" # this will set the root-node for our view
|
23
|
+
|
24
|
+
# `on` takes an event, and optionally a selector
|
25
|
+
on(:click, 'button') do |event| # leaving the selector black will listen for events
|
26
|
+
# on the root-node
|
27
|
+
|
28
|
+
@greeting_text = find('input').text # `find` operates only on inside the root-node
|
29
|
+
|
30
|
+
render! # `render!` will execute the render method below
|
31
|
+
end # wrapped in `requestAnimationFrame()`
|
32
|
+
|
33
|
+
def self.render # where possible is advisable to separate data collection
|
34
|
+
find('.output').text = @greeting_text # from the rendering/updating of the node, this has been
|
35
|
+
end # proven almost invariably useful in managing UI interactions
|
36
|
+
end
|
37
|
+
|
38
|
+
Receptive::App.run
|
39
|
+
```
|
40
|
+
|
41
|
+
---
|
42
|
+
|
43
|
+
*upcoming chapters…*
|
44
|
+
|
45
|
+
## Application lifecycle
|
46
|
+
## Incremental DOM
|
47
|
+
## The `render!` method
|
48
|
+
## `window` and `document` events
|
49
|
+
## Why still jQuery?
|
50
|
+
## Using actions and the global status
|
51
|
+
|
52
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Elia Schito
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Receptive
|
2
|
+
|
3
|
+
Receptive is a toolkit that will help you add behavior to your existing, server-generated HTML. It's not intended for single-page-application but rather about taking care of specific DOM nodes.
|
4
|
+
|
5
|
+
This is perfect for you if:
|
6
|
+
|
7
|
+
- **you already generate all your views from the server** (and don't want to throw everything away following the latest JavaScript fad)
|
8
|
+
- **you need your site to work without JavaScript** for SEO or any other reasons
|
9
|
+
- **you have a bunch of jQuery stuff around** and always wanted to organize it properly
|
10
|
+
- **you like Ruby** and don't want to get caught by all the subtle quirks of JavaScript
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'receptive'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
|
26
|
+
## How it works
|
27
|
+
|
28
|
+
Just keep your HTML as it is:
|
29
|
+
|
30
|
+
```html
|
31
|
+
<div class="hello-world">
|
32
|
+
<input type="text">
|
33
|
+
<button>Greet</button>
|
34
|
+
<span class="output"></span>
|
35
|
+
</div>
|
36
|
+
```
|
37
|
+
|
38
|
+
Then write a view with a reference to it:
|
39
|
+
|
40
|
+
```rb
|
41
|
+
class HelloWorld
|
42
|
+
extend Receptive::View
|
43
|
+
self.selector = ".hello-world"
|
44
|
+
|
45
|
+
on(:click, 'button') do |event|
|
46
|
+
@greeting_text = find('input').text
|
47
|
+
render!
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.render
|
51
|
+
find('.output').text = @greeting_text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Read on in the [handbook](./HANDBOOK.md)
|
57
|
+
|
58
|
+
## Other features
|
59
|
+
|
60
|
+
- script[async] compatible
|
61
|
+
- pjax/turbolinks compatible
|
62
|
+
|
63
|
+
|
64
|
+
## Development
|
65
|
+
|
66
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
67
|
+
|
68
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/elia/receptive.
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test_mri) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
end
|
9
|
+
|
10
|
+
task :test_opal do
|
11
|
+
require 'opal'
|
12
|
+
ENV['RUBYOPT'] += ' -rbundler/setup -ropal/minitest '
|
13
|
+
files = nil
|
14
|
+
cd('test-opal') do
|
15
|
+
files = Dir['**/*_test.rb'].map{|f| "-r#{f.shellescape}"}
|
16
|
+
end
|
17
|
+
sh "opal -Ilib-opal -Itest-opal #{files.join(' ')} -e ':done'"
|
18
|
+
end
|
19
|
+
|
20
|
+
task :test => [:test_mri, :test_opal]
|
21
|
+
|
22
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "receptive"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'receptive/view'
|
2
|
+
|
3
|
+
module Receptive::App
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def trigger(event_name, data = nil)
|
7
|
+
$console.log(
|
8
|
+
"%c event %c #{event_name} %c ",
|
9
|
+
'background-color:#eee;color:#444;border-radius:2px 0 0 2px',
|
10
|
+
'background-color:#94D1F5;color:#0A3EA3;border-radius:0 2px 2px 0',
|
11
|
+
'background-color:none;border:none;font-family:monospace;',
|
12
|
+
`data != null && data.$inspect && data.$inspect()`,
|
13
|
+
self
|
14
|
+
) if $DEBUG
|
15
|
+
Document.trigger(event_name, data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def on(event_name, &block)
|
19
|
+
Document.on(event_name, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
trigger('app:starting', self)
|
24
|
+
::Receptive::View.extenders.each { |c| c.setup_persistent_events }
|
25
|
+
trigger('app:started', self)
|
26
|
+
|
27
|
+
# Managing dom:ready for sync & async script tags
|
28
|
+
app_loaded = ->*{ trigger('app:loaded') }
|
29
|
+
|
30
|
+
%x{
|
31
|
+
if (document.readyState === 'complete') {
|
32
|
+
setTimeout(#{app_loaded}, 1);
|
33
|
+
} else {
|
34
|
+
document.addEventListener('DOMContentLoaded', #{app_loaded}, false);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
# Managing visibility changes
|
39
|
+
visibility_change = -> { trigger(`document`.JS[:hidden] ? 'visibility:hidden' : 'visibility:visible') }
|
40
|
+
%x{
|
41
|
+
if (!document.hidden) {
|
42
|
+
setTimeout(#{visibility_change}, 1);
|
43
|
+
} else {
|
44
|
+
document.addEventListener('visibilitychange', #{visibility_change}, false);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,1223 @@
|
|
1
|
+
// https://ajax.googleapis.com/ajax/libs/incrementaldom/0.5.1/incremental-dom.js
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @license
|
5
|
+
* Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
|
6
|
+
*
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
* you may not use this file except in compliance with the License.
|
9
|
+
* You may obtain a copy of the License at
|
10
|
+
*
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
*
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
14
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
* See the License for the specific language governing permissions and
|
17
|
+
* limitations under the License.
|
18
|
+
*/
|
19
|
+
|
20
|
+
(function (global, factory) {
|
21
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
22
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
23
|
+
(factory((global.IncrementalDOM = {})));
|
24
|
+
}(this, function (exports) { 'use strict';
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
|
28
|
+
*
|
29
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
30
|
+
* you may not use this file except in compliance with the License.
|
31
|
+
* You may obtain a copy of the License at
|
32
|
+
*
|
33
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
34
|
+
*
|
35
|
+
* Unless required by applicable law or agreed to in writing, software
|
36
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
37
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
38
|
+
* See the License for the specific language governing permissions and
|
39
|
+
* limitations under the License.
|
40
|
+
*/
|
41
|
+
|
42
|
+
/**
|
43
|
+
* A cached reference to the hasOwnProperty function.
|
44
|
+
*/
|
45
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
46
|
+
|
47
|
+
/**
|
48
|
+
* A constructor function that will create blank objects.
|
49
|
+
* @constructor
|
50
|
+
*/
|
51
|
+
function Blank() {}
|
52
|
+
|
53
|
+
Blank.prototype = Object.create(null);
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Used to prevent property collisions between our "map" and its prototype.
|
57
|
+
* @param {!Object<string, *>} map The map to check.
|
58
|
+
* @param {string} property The property to check.
|
59
|
+
* @return {boolean} Whether map has property.
|
60
|
+
*/
|
61
|
+
var has = function (map, property) {
|
62
|
+
return hasOwnProperty.call(map, property);
|
63
|
+
};
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Creates an map object without a prototype.
|
67
|
+
* @return {!Object}
|
68
|
+
*/
|
69
|
+
var createMap = function () {
|
70
|
+
return new Blank();
|
71
|
+
};
|
72
|
+
|
73
|
+
/**
|
74
|
+
* The property name where we store Incremental DOM data.
|
75
|
+
*/
|
76
|
+
var DATA_PROP = '__incrementalDOMData';
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Keeps track of information needed to perform diffs for a given DOM node.
|
80
|
+
* @param {!string} nodeName
|
81
|
+
* @param {?string=} key
|
82
|
+
* @constructor
|
83
|
+
*/
|
84
|
+
function NodeData(nodeName, key) {
|
85
|
+
/**
|
86
|
+
* The attributes and their values.
|
87
|
+
* @const {!Object<string, *>}
|
88
|
+
*/
|
89
|
+
this.attrs = createMap();
|
90
|
+
|
91
|
+
/**
|
92
|
+
* An array of attribute name/value pairs, used for quickly diffing the
|
93
|
+
* incomming attributes to see if the DOM node's attributes need to be
|
94
|
+
* updated.
|
95
|
+
* @const {Array<*>}
|
96
|
+
*/
|
97
|
+
this.attrsArr = [];
|
98
|
+
|
99
|
+
/**
|
100
|
+
* The incoming attributes for this Node, before they are updated.
|
101
|
+
* @const {!Object<string, *>}
|
102
|
+
*/
|
103
|
+
this.newAttrs = createMap();
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Whether or not the statics have been applied for the node yet.
|
107
|
+
* {boolean}
|
108
|
+
*/
|
109
|
+
this.staticsApplied = false;
|
110
|
+
|
111
|
+
/**
|
112
|
+
* The key used to identify this node, used to preserve DOM nodes when they
|
113
|
+
* move within their parent.
|
114
|
+
* @const
|
115
|
+
*/
|
116
|
+
this.key = key;
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Keeps track of children within this node by their key.
|
120
|
+
* {!Object<string, !Element>}
|
121
|
+
*/
|
122
|
+
this.keyMap = createMap();
|
123
|
+
|
124
|
+
/**
|
125
|
+
* Whether or not the keyMap is currently valid.
|
126
|
+
* @type {boolean}
|
127
|
+
*/
|
128
|
+
this.keyMapValid = true;
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Whether or the associated node is, or contains, a focused Element.
|
132
|
+
* @type {boolean}
|
133
|
+
*/
|
134
|
+
this.focused = false;
|
135
|
+
|
136
|
+
/**
|
137
|
+
* The node name for this node.
|
138
|
+
* @const {string}
|
139
|
+
*/
|
140
|
+
this.nodeName = nodeName;
|
141
|
+
|
142
|
+
/**
|
143
|
+
* @type {?string}
|
144
|
+
*/
|
145
|
+
this.text = null;
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Initializes a NodeData object for a Node.
|
150
|
+
*
|
151
|
+
* @param {Node} node The node to initialize data for.
|
152
|
+
* @param {string} nodeName The node name of node.
|
153
|
+
* @param {?string=} key The key that identifies the node.
|
154
|
+
* @return {!NodeData} The newly initialized data object
|
155
|
+
*/
|
156
|
+
var initData = function (node, nodeName, key) {
|
157
|
+
var data = new NodeData(nodeName, key);
|
158
|
+
node[DATA_PROP] = data;
|
159
|
+
return data;
|
160
|
+
};
|
161
|
+
|
162
|
+
/**
|
163
|
+
* Retrieves the NodeData object for a Node, creating it if necessary.
|
164
|
+
*
|
165
|
+
* @param {?Node} node The Node to retrieve the data for.
|
166
|
+
* @return {!NodeData} The NodeData for this Node.
|
167
|
+
*/
|
168
|
+
var getData = function (node) {
|
169
|
+
importNode(node);
|
170
|
+
return node[DATA_PROP];
|
171
|
+
};
|
172
|
+
|
173
|
+
/**
|
174
|
+
* Imports node and its subtree, initializing caches.
|
175
|
+
*
|
176
|
+
* @param {?Node} node The Node to import.
|
177
|
+
*/
|
178
|
+
var importNode = function (node) {
|
179
|
+
if (node[DATA_PROP]) {
|
180
|
+
return;
|
181
|
+
}
|
182
|
+
|
183
|
+
var isElement = node instanceof Element;
|
184
|
+
var nodeName = isElement ? node.localName : node.nodeName;
|
185
|
+
var key = isElement ? node.getAttribute('key') : null;
|
186
|
+
var data = initData(node, nodeName, key);
|
187
|
+
|
188
|
+
if (key) {
|
189
|
+
getData(node.parentNode).keyMap[key] = node;
|
190
|
+
}
|
191
|
+
|
192
|
+
if (isElement) {
|
193
|
+
var attributes = node.attributes;
|
194
|
+
var attrs = data.attrs;
|
195
|
+
var newAttrs = data.newAttrs;
|
196
|
+
var attrsArr = data.attrsArr;
|
197
|
+
|
198
|
+
for (var i = 0; i < attributes.length; i += 1) {
|
199
|
+
var attr = attributes[i];
|
200
|
+
var name = attr.name;
|
201
|
+
var value = attr.value;
|
202
|
+
|
203
|
+
attrs[name] = value;
|
204
|
+
newAttrs[name] = undefined;
|
205
|
+
attrsArr.push(name);
|
206
|
+
attrsArr.push(value);
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
for (var child = node.firstChild; child; child = child.nextSibling) {
|
211
|
+
importNode(child);
|
212
|
+
}
|
213
|
+
};
|
214
|
+
|
215
|
+
/**
|
216
|
+
* Gets the namespace to create an element (of a given tag) in.
|
217
|
+
* @param {string} tag The tag to get the namespace for.
|
218
|
+
* @param {?Node} parent
|
219
|
+
* @return {?string} The namespace to create the tag in.
|
220
|
+
*/
|
221
|
+
var getNamespaceForTag = function (tag, parent) {
|
222
|
+
if (tag === 'svg') {
|
223
|
+
return 'http://www.w3.org/2000/svg';
|
224
|
+
}
|
225
|
+
|
226
|
+
if (getData(parent).nodeName === 'foreignObject') {
|
227
|
+
return null;
|
228
|
+
}
|
229
|
+
|
230
|
+
return parent.namespaceURI;
|
231
|
+
};
|
232
|
+
|
233
|
+
/**
|
234
|
+
* Creates an Element.
|
235
|
+
* @param {Document} doc The document with which to create the Element.
|
236
|
+
* @param {?Node} parent
|
237
|
+
* @param {string} tag The tag for the Element.
|
238
|
+
* @param {?string=} key A key to identify the Element.
|
239
|
+
* @return {!Element}
|
240
|
+
*/
|
241
|
+
var createElement = function (doc, parent, tag, key) {
|
242
|
+
var namespace = getNamespaceForTag(tag, parent);
|
243
|
+
var el = undefined;
|
244
|
+
|
245
|
+
if (namespace) {
|
246
|
+
el = doc.createElementNS(namespace, tag);
|
247
|
+
} else {
|
248
|
+
el = doc.createElement(tag);
|
249
|
+
}
|
250
|
+
|
251
|
+
initData(el, tag, key);
|
252
|
+
|
253
|
+
return el;
|
254
|
+
};
|
255
|
+
|
256
|
+
/**
|
257
|
+
* Creates a Text Node.
|
258
|
+
* @param {Document} doc The document with which to create the Element.
|
259
|
+
* @return {!Text}
|
260
|
+
*/
|
261
|
+
var createText = function (doc) {
|
262
|
+
var node = doc.createTextNode('');
|
263
|
+
initData(node, '#text', null);
|
264
|
+
return node;
|
265
|
+
};
|
266
|
+
|
267
|
+
/**
|
268
|
+
* Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
|
269
|
+
*
|
270
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
271
|
+
* you may not use this file except in compliance with the License.
|
272
|
+
* You may obtain a copy of the License at
|
273
|
+
*
|
274
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
275
|
+
*
|
276
|
+
* Unless required by applicable law or agreed to in writing, software
|
277
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
278
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
279
|
+
* See the License for the specific language governing permissions and
|
280
|
+
* limitations under the License.
|
281
|
+
*/
|
282
|
+
|
283
|
+
/** @const */
|
284
|
+
var notifications = {
|
285
|
+
/**
|
286
|
+
* Called after patch has compleated with any Nodes that have been created
|
287
|
+
* and added to the DOM.
|
288
|
+
* @type {?function(Array<!Node>)}
|
289
|
+
*/
|
290
|
+
nodesCreated: null,
|
291
|
+
|
292
|
+
/**
|
293
|
+
* Called after patch has compleated with any Nodes that have been removed
|
294
|
+
* from the DOM.
|
295
|
+
* Note it's an applications responsibility to handle any childNodes.
|
296
|
+
* @type {?function(Array<!Node>)}
|
297
|
+
*/
|
298
|
+
nodesDeleted: null
|
299
|
+
};
|
300
|
+
|
301
|
+
/**
|
302
|
+
* Keeps track of the state of a patch.
|
303
|
+
* @constructor
|
304
|
+
*/
|
305
|
+
function Context() {
|
306
|
+
/**
|
307
|
+
* @type {(Array<!Node>|undefined)}
|
308
|
+
*/
|
309
|
+
this.created = notifications.nodesCreated && [];
|
310
|
+
|
311
|
+
/**
|
312
|
+
* @type {(Array<!Node>|undefined)}
|
313
|
+
*/
|
314
|
+
this.deleted = notifications.nodesDeleted && [];
|
315
|
+
}
|
316
|
+
|
317
|
+
/**
|
318
|
+
* @param {!Node} node
|
319
|
+
*/
|
320
|
+
Context.prototype.markCreated = function (node) {
|
321
|
+
if (this.created) {
|
322
|
+
this.created.push(node);
|
323
|
+
}
|
324
|
+
};
|
325
|
+
|
326
|
+
/**
|
327
|
+
* @param {!Node} node
|
328
|
+
*/
|
329
|
+
Context.prototype.markDeleted = function (node) {
|
330
|
+
if (this.deleted) {
|
331
|
+
this.deleted.push(node);
|
332
|
+
}
|
333
|
+
};
|
334
|
+
|
335
|
+
/**
|
336
|
+
* Notifies about nodes that were created during the patch opearation.
|
337
|
+
*/
|
338
|
+
Context.prototype.notifyChanges = function () {
|
339
|
+
if (this.created && this.created.length > 0) {
|
340
|
+
notifications.nodesCreated(this.created);
|
341
|
+
}
|
342
|
+
|
343
|
+
if (this.deleted && this.deleted.length > 0) {
|
344
|
+
notifications.nodesDeleted(this.deleted);
|
345
|
+
}
|
346
|
+
};
|
347
|
+
|
348
|
+
/**
|
349
|
+
* Copyright 2016 The Incremental DOM Authors. All Rights Reserved.
|
350
|
+
*
|
351
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
352
|
+
* you may not use this file except in compliance with the License.
|
353
|
+
* You may obtain a copy of the License at
|
354
|
+
*
|
355
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
356
|
+
*
|
357
|
+
* Unless required by applicable law or agreed to in writing, software
|
358
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
359
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
360
|
+
* See the License for the specific language governing permissions and
|
361
|
+
* limitations under the License.
|
362
|
+
*/
|
363
|
+
|
364
|
+
/**
|
365
|
+
* @param {!Node} node
|
366
|
+
* @return {boolean} True if the node the root of a document, false otherwise.
|
367
|
+
*/
|
368
|
+
var isDocumentRoot = function (node) {
|
369
|
+
// For ShadowRoots, check if they are a DocumentFragment instead of if they
|
370
|
+
// are a ShadowRoot so that this can work in 'use strict' if ShadowRoots are
|
371
|
+
// not supported.
|
372
|
+
return node instanceof Document || node instanceof DocumentFragment;
|
373
|
+
};
|
374
|
+
|
375
|
+
/**
|
376
|
+
* @param {!Node} node The node to start at, inclusive.
|
377
|
+
* @param {?Node} root The root ancestor to get until, exclusive.
|
378
|
+
* @return {!Array<!Node>} The ancestry of DOM nodes.
|
379
|
+
*/
|
380
|
+
var getAncestry = function (node, root) {
|
381
|
+
var ancestry = [];
|
382
|
+
var cur = node;
|
383
|
+
|
384
|
+
while (cur !== root) {
|
385
|
+
ancestry.push(cur);
|
386
|
+
cur = cur.parentNode;
|
387
|
+
}
|
388
|
+
|
389
|
+
return ancestry;
|
390
|
+
};
|
391
|
+
|
392
|
+
/**
|
393
|
+
* @param {!Node} node
|
394
|
+
* @return {!Node} The root node of the DOM tree that contains node.
|
395
|
+
*/
|
396
|
+
var getRoot = function (node) {
|
397
|
+
var cur = node;
|
398
|
+
var prev = cur;
|
399
|
+
|
400
|
+
while (cur) {
|
401
|
+
prev = cur;
|
402
|
+
cur = cur.parentNode;
|
403
|
+
}
|
404
|
+
|
405
|
+
return prev;
|
406
|
+
};
|
407
|
+
|
408
|
+
/**
|
409
|
+
* @param {!Node} node The node to get the activeElement for.
|
410
|
+
* @return {?Element} The activeElement in the Document or ShadowRoot
|
411
|
+
* corresponding to node, if present.
|
412
|
+
*/
|
413
|
+
var getActiveElement = function (node) {
|
414
|
+
var root = getRoot(node);
|
415
|
+
return isDocumentRoot(root) ? root.activeElement : null;
|
416
|
+
};
|
417
|
+
|
418
|
+
/**
|
419
|
+
* Gets the path of nodes that contain the focused node in the same document as
|
420
|
+
* a reference node, up until the root.
|
421
|
+
* @param {!Node} node The reference node to get the activeElement for.
|
422
|
+
* @param {?Node} root The root to get the focused path until.
|
423
|
+
* @return {!Array<Node>}
|
424
|
+
*/
|
425
|
+
var getFocusedPath = function (node, root) {
|
426
|
+
var activeElement = getActiveElement(node);
|
427
|
+
|
428
|
+
if (!activeElement || !node.contains(activeElement)) {
|
429
|
+
return [];
|
430
|
+
}
|
431
|
+
|
432
|
+
return getAncestry(activeElement, root);
|
433
|
+
};
|
434
|
+
|
435
|
+
/**
|
436
|
+
* Like insertBefore, but instead instead of moving the desired node, instead
|
437
|
+
* moves all the other nodes after.
|
438
|
+
* @param {?Node} parentNode
|
439
|
+
* @param {!Node} node
|
440
|
+
* @param {?Node} referenceNode
|
441
|
+
*/
|
442
|
+
var moveBefore = function (parentNode, node, referenceNode) {
|
443
|
+
var insertReferenceNode = node.nextSibling;
|
444
|
+
var cur = referenceNode;
|
445
|
+
|
446
|
+
while (cur !== node) {
|
447
|
+
var next = cur.nextSibling;
|
448
|
+
parentNode.insertBefore(cur, insertReferenceNode);
|
449
|
+
cur = next;
|
450
|
+
}
|
451
|
+
};
|
452
|
+
|
453
|
+
/** @type {?Context} */
|
454
|
+
var context = null;
|
455
|
+
|
456
|
+
/** @type {?Node} */
|
457
|
+
var currentNode = null;
|
458
|
+
|
459
|
+
/** @type {?Node} */
|
460
|
+
var currentParent = null;
|
461
|
+
|
462
|
+
/** @type {?Document} */
|
463
|
+
var doc = null;
|
464
|
+
|
465
|
+
/**
|
466
|
+
* @param {!Array<Node>} focusPath The nodes to mark.
|
467
|
+
* @param {boolean} focused Whether or not they are focused.
|
468
|
+
*/
|
469
|
+
var markFocused = function (focusPath, focused) {
|
470
|
+
for (var i = 0; i < focusPath.length; i += 1) {
|
471
|
+
getData(focusPath[i]).focused = focused;
|
472
|
+
}
|
473
|
+
};
|
474
|
+
|
475
|
+
/**
|
476
|
+
* Returns a patcher function that sets up and restores a patch context,
|
477
|
+
* running the run function with the provided data.
|
478
|
+
* @param {function((!Element|!DocumentFragment),!function(T),T=): ?Node} run
|
479
|
+
* @return {function((!Element|!DocumentFragment),!function(T),T=): ?Node}
|
480
|
+
* @template T
|
481
|
+
*/
|
482
|
+
var patchFactory = function (run) {
|
483
|
+
/**
|
484
|
+
* TODO(moz): These annotations won't be necessary once we switch to Closure
|
485
|
+
* Compiler's new type inference. Remove these once the switch is done.
|
486
|
+
*
|
487
|
+
* @param {(!Element|!DocumentFragment)} node
|
488
|
+
* @param {!function(T)} fn
|
489
|
+
* @param {T=} data
|
490
|
+
* @return {?Node} node
|
491
|
+
* @template T
|
492
|
+
*/
|
493
|
+
var f = function (node, fn, data) {
|
494
|
+
var prevContext = context;
|
495
|
+
var prevDoc = doc;
|
496
|
+
var prevCurrentNode = currentNode;
|
497
|
+
var prevCurrentParent = currentParent;
|
498
|
+
var previousInAttributes = false;
|
499
|
+
var previousInSkip = false;
|
500
|
+
|
501
|
+
context = new Context();
|
502
|
+
doc = node.ownerDocument;
|
503
|
+
currentParent = node.parentNode;
|
504
|
+
|
505
|
+
if ('production' !== 'production') {}
|
506
|
+
|
507
|
+
var focusPath = getFocusedPath(node, currentParent);
|
508
|
+
markFocused(focusPath, true);
|
509
|
+
var retVal = run(node, fn, data);
|
510
|
+
markFocused(focusPath, false);
|
511
|
+
|
512
|
+
if ('production' !== 'production') {}
|
513
|
+
|
514
|
+
context.notifyChanges();
|
515
|
+
|
516
|
+
context = prevContext;
|
517
|
+
doc = prevDoc;
|
518
|
+
currentNode = prevCurrentNode;
|
519
|
+
currentParent = prevCurrentParent;
|
520
|
+
|
521
|
+
return retVal;
|
522
|
+
};
|
523
|
+
return f;
|
524
|
+
};
|
525
|
+
|
526
|
+
/**
|
527
|
+
* Patches the document starting at node with the provided function. This
|
528
|
+
* function may be called during an existing patch operation.
|
529
|
+
* @param {!Element|!DocumentFragment} node The Element or Document
|
530
|
+
* to patch.
|
531
|
+
* @param {!function(T)} fn A function containing elementOpen/elementClose/etc.
|
532
|
+
* calls that describe the DOM.
|
533
|
+
* @param {T=} data An argument passed to fn to represent DOM state.
|
534
|
+
* @return {!Node} The patched node.
|
535
|
+
* @template T
|
536
|
+
*/
|
537
|
+
var patchInner = patchFactory(function (node, fn, data) {
|
538
|
+
currentNode = node;
|
539
|
+
|
540
|
+
enterNode();
|
541
|
+
fn(data);
|
542
|
+
exitNode();
|
543
|
+
|
544
|
+
if ('production' !== 'production') {}
|
545
|
+
|
546
|
+
return node;
|
547
|
+
});
|
548
|
+
|
549
|
+
/**
|
550
|
+
* Patches an Element with the the provided function. Exactly one top level
|
551
|
+
* element call should be made corresponding to `node`.
|
552
|
+
* @param {!Element} node The Element where the patch should start.
|
553
|
+
* @param {!function(T)} fn A function containing elementOpen/elementClose/etc.
|
554
|
+
* calls that describe the DOM. This should have at most one top level
|
555
|
+
* element call.
|
556
|
+
* @param {T=} data An argument passed to fn to represent DOM state.
|
557
|
+
* @return {?Node} The node if it was updated, its replacedment or null if it
|
558
|
+
* was removed.
|
559
|
+
* @template T
|
560
|
+
*/
|
561
|
+
var patchOuter = patchFactory(function (node, fn, data) {
|
562
|
+
var startNode = /** @type {!Element} */{ nextSibling: node };
|
563
|
+
var expectedNextNode = null;
|
564
|
+
var expectedPrevNode = null;
|
565
|
+
|
566
|
+
if ('production' !== 'production') {}
|
567
|
+
|
568
|
+
currentNode = startNode;
|
569
|
+
fn(data);
|
570
|
+
|
571
|
+
if ('production' !== 'production') {}
|
572
|
+
|
573
|
+
if (node !== currentNode && node.parentNode) {
|
574
|
+
removeChild(currentParent, node, getData(currentParent).keyMap);
|
575
|
+
}
|
576
|
+
|
577
|
+
return startNode === currentNode ? null : currentNode;
|
578
|
+
});
|
579
|
+
|
580
|
+
/**
|
581
|
+
* Checks whether or not the current node matches the specified nodeName and
|
582
|
+
* key.
|
583
|
+
*
|
584
|
+
* @param {!Node} matchNode A node to match the data to.
|
585
|
+
* @param {?string} nodeName The nodeName for this node.
|
586
|
+
* @param {?string=} key An optional key that identifies a node.
|
587
|
+
* @return {boolean} True if the node matches, false otherwise.
|
588
|
+
*/
|
589
|
+
var matches = function (matchNode, nodeName, key) {
|
590
|
+
var data = getData(matchNode);
|
591
|
+
|
592
|
+
// Key check is done using double equals as we want to treat a null key the
|
593
|
+
// same as undefined. This should be okay as the only values allowed are
|
594
|
+
// strings, null and undefined so the == semantics are not too weird.
|
595
|
+
return nodeName === data.nodeName && key == data.key;
|
596
|
+
};
|
597
|
+
|
598
|
+
/**
|
599
|
+
* Aligns the virtual Element definition with the actual DOM, moving the
|
600
|
+
* corresponding DOM node to the correct location or creating it if necessary.
|
601
|
+
* @param {string} nodeName For an Element, this should be a valid tag string.
|
602
|
+
* For a Text, this should be #text.
|
603
|
+
* @param {?string=} key The key used to identify this element.
|
604
|
+
*/
|
605
|
+
var alignWithDOM = function (nodeName, key) {
|
606
|
+
if (currentNode && matches(currentNode, nodeName, key)) {
|
607
|
+
return;
|
608
|
+
}
|
609
|
+
|
610
|
+
var parentData = getData(currentParent);
|
611
|
+
var currentNodeData = currentNode && getData(currentNode);
|
612
|
+
var keyMap = parentData.keyMap;
|
613
|
+
var node = undefined;
|
614
|
+
|
615
|
+
// Check to see if the node has moved within the parent.
|
616
|
+
if (key) {
|
617
|
+
var keyNode = keyMap[key];
|
618
|
+
if (keyNode) {
|
619
|
+
if (matches(keyNode, nodeName, key)) {
|
620
|
+
node = keyNode;
|
621
|
+
} else if (keyNode === currentNode) {
|
622
|
+
context.markDeleted(keyNode);
|
623
|
+
} else {
|
624
|
+
removeChild(currentParent, keyNode, keyMap);
|
625
|
+
}
|
626
|
+
}
|
627
|
+
}
|
628
|
+
|
629
|
+
// Create the node if it doesn't exist.
|
630
|
+
if (!node) {
|
631
|
+
if (nodeName === '#text') {
|
632
|
+
node = createText(doc);
|
633
|
+
} else {
|
634
|
+
node = createElement(doc, currentParent, nodeName, key);
|
635
|
+
}
|
636
|
+
|
637
|
+
if (key) {
|
638
|
+
keyMap[key] = node;
|
639
|
+
}
|
640
|
+
|
641
|
+
context.markCreated(node);
|
642
|
+
}
|
643
|
+
|
644
|
+
// Re-order the node into the right position, preserving focus if either
|
645
|
+
// node or currentNode are focused by making sure that they are not detached
|
646
|
+
// from the DOM.
|
647
|
+
if (getData(node).focused) {
|
648
|
+
// Move everything else before the node.
|
649
|
+
moveBefore(currentParent, node, currentNode);
|
650
|
+
} else if (currentNodeData && currentNodeData.key && !currentNodeData.focused) {
|
651
|
+
// Remove the currentNode, which can always be added back since we hold a
|
652
|
+
// reference through the keyMap. This prevents a large number of moves when
|
653
|
+
// a keyed item is removed or moved backwards in the DOM.
|
654
|
+
currentParent.replaceChild(node, currentNode);
|
655
|
+
parentData.keyMapValid = false;
|
656
|
+
} else {
|
657
|
+
currentParent.insertBefore(node, currentNode);
|
658
|
+
}
|
659
|
+
|
660
|
+
currentNode = node;
|
661
|
+
};
|
662
|
+
|
663
|
+
/**
|
664
|
+
* @param {?Node} node
|
665
|
+
* @param {?Node} child
|
666
|
+
* @param {?Object<string, !Element>} keyMap
|
667
|
+
*/
|
668
|
+
var removeChild = function (node, child, keyMap) {
|
669
|
+
node.removeChild(child);
|
670
|
+
context.markDeleted( /** @type {!Node}*/child);
|
671
|
+
|
672
|
+
var key = getData(child).key;
|
673
|
+
if (key) {
|
674
|
+
delete keyMap[key];
|
675
|
+
}
|
676
|
+
};
|
677
|
+
|
678
|
+
/**
|
679
|
+
* Clears out any unvisited Nodes, as the corresponding virtual element
|
680
|
+
* functions were never called for them.
|
681
|
+
*/
|
682
|
+
var clearUnvisitedDOM = function () {
|
683
|
+
var node = currentParent;
|
684
|
+
var data = getData(node);
|
685
|
+
var keyMap = data.keyMap;
|
686
|
+
var keyMapValid = data.keyMapValid;
|
687
|
+
var child = node.lastChild;
|
688
|
+
var key = undefined;
|
689
|
+
|
690
|
+
if (child === currentNode && keyMapValid) {
|
691
|
+
return;
|
692
|
+
}
|
693
|
+
|
694
|
+
while (child !== currentNode) {
|
695
|
+
removeChild(node, child, keyMap);
|
696
|
+
child = node.lastChild;
|
697
|
+
}
|
698
|
+
|
699
|
+
// Clean the keyMap, removing any unusued keys.
|
700
|
+
if (!keyMapValid) {
|
701
|
+
for (key in keyMap) {
|
702
|
+
child = keyMap[key];
|
703
|
+
if (child.parentNode !== node) {
|
704
|
+
context.markDeleted(child);
|
705
|
+
delete keyMap[key];
|
706
|
+
}
|
707
|
+
}
|
708
|
+
|
709
|
+
data.keyMapValid = true;
|
710
|
+
}
|
711
|
+
};
|
712
|
+
|
713
|
+
/**
|
714
|
+
* Changes to the first child of the current node.
|
715
|
+
*/
|
716
|
+
var enterNode = function () {
|
717
|
+
currentParent = currentNode;
|
718
|
+
currentNode = null;
|
719
|
+
};
|
720
|
+
|
721
|
+
/**
|
722
|
+
* @return {?Node} The next Node to be patched.
|
723
|
+
*/
|
724
|
+
var getNextNode = function () {
|
725
|
+
if (currentNode) {
|
726
|
+
return currentNode.nextSibling;
|
727
|
+
} else {
|
728
|
+
return currentParent.firstChild;
|
729
|
+
}
|
730
|
+
};
|
731
|
+
|
732
|
+
/**
|
733
|
+
* Changes to the next sibling of the current node.
|
734
|
+
*/
|
735
|
+
var nextNode = function () {
|
736
|
+
currentNode = getNextNode();
|
737
|
+
};
|
738
|
+
|
739
|
+
/**
|
740
|
+
* Changes to the parent of the current node, removing any unvisited children.
|
741
|
+
*/
|
742
|
+
var exitNode = function () {
|
743
|
+
clearUnvisitedDOM();
|
744
|
+
|
745
|
+
currentNode = currentParent;
|
746
|
+
currentParent = currentParent.parentNode;
|
747
|
+
};
|
748
|
+
|
749
|
+
/**
|
750
|
+
* Makes sure that the current node is an Element with a matching tagName and
|
751
|
+
* key.
|
752
|
+
*
|
753
|
+
* @param {string} tag The element's tag.
|
754
|
+
* @param {?string=} key The key used to identify this element. This can be an
|
755
|
+
* empty string, but performance may be better if a unique value is used
|
756
|
+
* when iterating over an array of items.
|
757
|
+
* @return {!Element} The corresponding Element.
|
758
|
+
*/
|
759
|
+
var coreElementOpen = function (tag, key) {
|
760
|
+
nextNode();
|
761
|
+
alignWithDOM(tag, key);
|
762
|
+
enterNode();
|
763
|
+
return (/** @type {!Element} */currentParent
|
764
|
+
);
|
765
|
+
};
|
766
|
+
|
767
|
+
/**
|
768
|
+
* Closes the currently open Element, removing any unvisited children if
|
769
|
+
* necessary.
|
770
|
+
*
|
771
|
+
* @return {!Element} The corresponding Element.
|
772
|
+
*/
|
773
|
+
var coreElementClose = function () {
|
774
|
+
if ('production' !== 'production') {}
|
775
|
+
|
776
|
+
exitNode();
|
777
|
+
return (/** @type {!Element} */currentNode
|
778
|
+
);
|
779
|
+
};
|
780
|
+
|
781
|
+
/**
|
782
|
+
* Makes sure the current node is a Text node and creates a Text node if it is
|
783
|
+
* not.
|
784
|
+
*
|
785
|
+
* @return {!Text} The corresponding Text Node.
|
786
|
+
*/
|
787
|
+
var coreText = function () {
|
788
|
+
nextNode();
|
789
|
+
alignWithDOM('#text', null);
|
790
|
+
return (/** @type {!Text} */currentNode
|
791
|
+
);
|
792
|
+
};
|
793
|
+
|
794
|
+
/**
|
795
|
+
* Gets the current Element being patched.
|
796
|
+
* @return {!Element}
|
797
|
+
*/
|
798
|
+
var currentElement = function () {
|
799
|
+
if ('production' !== 'production') {}
|
800
|
+
return (/** @type {!Element} */currentParent
|
801
|
+
);
|
802
|
+
};
|
803
|
+
|
804
|
+
/**
|
805
|
+
* @return {Node} The Node that will be evaluated for the next instruction.
|
806
|
+
*/
|
807
|
+
var currentPointer = function () {
|
808
|
+
if ('production' !== 'production') {}
|
809
|
+
return getNextNode();
|
810
|
+
};
|
811
|
+
|
812
|
+
/**
|
813
|
+
* Skips the children in a subtree, allowing an Element to be closed without
|
814
|
+
* clearing out the children.
|
815
|
+
*/
|
816
|
+
var skip = function () {
|
817
|
+
if ('production' !== 'production') {}
|
818
|
+
currentNode = currentParent.lastChild;
|
819
|
+
};
|
820
|
+
|
821
|
+
/**
|
822
|
+
* Skips the next Node to be patched, moving the pointer forward to the next
|
823
|
+
* sibling of the current pointer.
|
824
|
+
*/
|
825
|
+
var skipNode = nextNode;
|
826
|
+
|
827
|
+
/**
|
828
|
+
* Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
|
829
|
+
*
|
830
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
831
|
+
* you may not use this file except in compliance with the License.
|
832
|
+
* You may obtain a copy of the License at
|
833
|
+
*
|
834
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
835
|
+
*
|
836
|
+
* Unless required by applicable law or agreed to in writing, software
|
837
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
838
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
839
|
+
* See the License for the specific language governing permissions and
|
840
|
+
* limitations under the License.
|
841
|
+
*/
|
842
|
+
|
843
|
+
/** @const */
|
844
|
+
var symbols = {
|
845
|
+
default: '__default'
|
846
|
+
};
|
847
|
+
|
848
|
+
/**
|
849
|
+
* @param {string} name
|
850
|
+
* @return {string|undefined} The namespace to use for the attribute.
|
851
|
+
*/
|
852
|
+
var getNamespace = function (name) {
|
853
|
+
if (name.lastIndexOf('xml:', 0) === 0) {
|
854
|
+
return 'http://www.w3.org/XML/1998/namespace';
|
855
|
+
}
|
856
|
+
|
857
|
+
if (name.lastIndexOf('xlink:', 0) === 0) {
|
858
|
+
return 'http://www.w3.org/1999/xlink';
|
859
|
+
}
|
860
|
+
};
|
861
|
+
|
862
|
+
/**
|
863
|
+
* Applies an attribute or property to a given Element. If the value is null
|
864
|
+
* or undefined, it is removed from the Element. Otherwise, the value is set
|
865
|
+
* as an attribute.
|
866
|
+
* @param {!Element} el
|
867
|
+
* @param {string} name The attribute's name.
|
868
|
+
* @param {?(boolean|number|string)=} value The attribute's value.
|
869
|
+
*/
|
870
|
+
var applyAttr = function (el, name, value) {
|
871
|
+
if (value == null) {
|
872
|
+
el.removeAttribute(name);
|
873
|
+
} else {
|
874
|
+
var attrNS = getNamespace(name);
|
875
|
+
if (attrNS) {
|
876
|
+
el.setAttributeNS(attrNS, name, value);
|
877
|
+
} else {
|
878
|
+
el.setAttribute(name, value);
|
879
|
+
}
|
880
|
+
}
|
881
|
+
};
|
882
|
+
|
883
|
+
/**
|
884
|
+
* Applies a property to a given Element.
|
885
|
+
* @param {!Element} el
|
886
|
+
* @param {string} name The property's name.
|
887
|
+
* @param {*} value The property's value.
|
888
|
+
*/
|
889
|
+
var applyProp = function (el, name, value) {
|
890
|
+
el[name] = value;
|
891
|
+
};
|
892
|
+
|
893
|
+
/**
|
894
|
+
* Applies a value to a style declaration. Supports CSS custom properties by
|
895
|
+
* setting properties containing a dash using CSSStyleDeclaration.setProperty.
|
896
|
+
* @param {CSSStyleDeclaration} style
|
897
|
+
* @param {!string} prop
|
898
|
+
* @param {*} value
|
899
|
+
*/
|
900
|
+
var setStyleValue = function (style, prop, value) {
|
901
|
+
if (prop.indexOf('-') >= 0) {
|
902
|
+
style.setProperty(prop, /** @type {string} */value);
|
903
|
+
} else {
|
904
|
+
style[prop] = value;
|
905
|
+
}
|
906
|
+
};
|
907
|
+
|
908
|
+
/**
|
909
|
+
* Applies a style to an Element. No vendor prefix expansion is done for
|
910
|
+
* property names/values.
|
911
|
+
* @param {!Element} el
|
912
|
+
* @param {string} name The attribute's name.
|
913
|
+
* @param {*} style The style to set. Either a string of css or an object
|
914
|
+
* containing property-value pairs.
|
915
|
+
*/
|
916
|
+
var applyStyle = function (el, name, style) {
|
917
|
+
if (typeof style === 'string') {
|
918
|
+
el.style.cssText = style;
|
919
|
+
} else {
|
920
|
+
el.style.cssText = '';
|
921
|
+
var elStyle = el.style;
|
922
|
+
var obj = /** @type {!Object<string,string>} */style;
|
923
|
+
|
924
|
+
for (var prop in obj) {
|
925
|
+
if (has(obj, prop)) {
|
926
|
+
setStyleValue(elStyle, prop, obj[prop]);
|
927
|
+
}
|
928
|
+
}
|
929
|
+
}
|
930
|
+
};
|
931
|
+
|
932
|
+
/**
|
933
|
+
* Updates a single attribute on an Element.
|
934
|
+
* @param {!Element} el
|
935
|
+
* @param {string} name The attribute's name.
|
936
|
+
* @param {*} value The attribute's value. If the value is an object or
|
937
|
+
* function it is set on the Element, otherwise, it is set as an HTML
|
938
|
+
* attribute.
|
939
|
+
*/
|
940
|
+
var applyAttributeTyped = function (el, name, value) {
|
941
|
+
var type = typeof value;
|
942
|
+
|
943
|
+
if (type === 'object' || type === 'function') {
|
944
|
+
applyProp(el, name, value);
|
945
|
+
} else {
|
946
|
+
applyAttr(el, name, /** @type {?(boolean|number|string)} */value);
|
947
|
+
}
|
948
|
+
};
|
949
|
+
|
950
|
+
/**
|
951
|
+
* Calls the appropriate attribute mutator for this attribute.
|
952
|
+
* @param {!Element} el
|
953
|
+
* @param {string} name The attribute's name.
|
954
|
+
* @param {*} value The attribute's value.
|
955
|
+
*/
|
956
|
+
var updateAttribute = function (el, name, value) {
|
957
|
+
var data = getData(el);
|
958
|
+
var attrs = data.attrs;
|
959
|
+
|
960
|
+
if (attrs[name] === value) {
|
961
|
+
return;
|
962
|
+
}
|
963
|
+
|
964
|
+
var mutator = attributes[name] || attributes[symbols.default];
|
965
|
+
mutator(el, name, value);
|
966
|
+
|
967
|
+
attrs[name] = value;
|
968
|
+
};
|
969
|
+
|
970
|
+
/**
|
971
|
+
* A publicly mutable object to provide custom mutators for attributes.
|
972
|
+
* @const {!Object<string, function(!Element, string, *)>}
|
973
|
+
*/
|
974
|
+
var attributes = createMap();
|
975
|
+
|
976
|
+
// Special generic mutator that's called for any attribute that does not
|
977
|
+
// have a specific mutator.
|
978
|
+
attributes[symbols.default] = applyAttributeTyped;
|
979
|
+
|
980
|
+
attributes['style'] = applyStyle;
|
981
|
+
|
982
|
+
/**
|
983
|
+
* The offset in the virtual element declaration where the attributes are
|
984
|
+
* specified.
|
985
|
+
* @const
|
986
|
+
*/
|
987
|
+
var ATTRIBUTES_OFFSET = 3;
|
988
|
+
|
989
|
+
/**
|
990
|
+
* Builds an array of arguments for use with elementOpenStart, attr and
|
991
|
+
* elementOpenEnd.
|
992
|
+
* @const {Array<*>}
|
993
|
+
*/
|
994
|
+
var argsBuilder = [];
|
995
|
+
|
996
|
+
/**
|
997
|
+
* @param {string} tag The element's tag.
|
998
|
+
* @param {?string=} key The key used to identify this element. This can be an
|
999
|
+
* empty string, but performance may be better if a unique value is used
|
1000
|
+
* when iterating over an array of items.
|
1001
|
+
* @param {?Array<*>=} statics An array of attribute name/value pairs of the
|
1002
|
+
* static attributes for the Element. These will only be set once when the
|
1003
|
+
* Element is created.
|
1004
|
+
* @param {...*} var_args, Attribute name/value pairs of the dynamic attributes
|
1005
|
+
* for the Element.
|
1006
|
+
* @return {!Element} The corresponding Element.
|
1007
|
+
*/
|
1008
|
+
var elementOpen = function (tag, key, statics, var_args) {
|
1009
|
+
if ('production' !== 'production') {}
|
1010
|
+
|
1011
|
+
var node = coreElementOpen(tag, key);
|
1012
|
+
var data = getData(node);
|
1013
|
+
|
1014
|
+
if (!data.staticsApplied) {
|
1015
|
+
if (statics) {
|
1016
|
+
for (var _i = 0; _i < statics.length; _i += 2) {
|
1017
|
+
var name = /** @type {string} */statics[_i];
|
1018
|
+
var value = statics[_i + 1];
|
1019
|
+
updateAttribute(node, name, value);
|
1020
|
+
}
|
1021
|
+
}
|
1022
|
+
// Down the road, we may want to keep track of the statics array to use it
|
1023
|
+
// as an additional signal about whether a node matches or not. For now,
|
1024
|
+
// just use a marker so that we do not reapply statics.
|
1025
|
+
data.staticsApplied = true;
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
/*
|
1029
|
+
* Checks to see if one or more attributes have changed for a given Element.
|
1030
|
+
* When no attributes have changed, this is much faster than checking each
|
1031
|
+
* individual argument. When attributes have changed, the overhead of this is
|
1032
|
+
* minimal.
|
1033
|
+
*/
|
1034
|
+
var attrsArr = data.attrsArr;
|
1035
|
+
var newAttrs = data.newAttrs;
|
1036
|
+
var isNew = !attrsArr.length;
|
1037
|
+
var i = ATTRIBUTES_OFFSET;
|
1038
|
+
var j = 0;
|
1039
|
+
|
1040
|
+
for (; i < arguments.length; i += 2, j += 2) {
|
1041
|
+
var _attr = arguments[i];
|
1042
|
+
if (isNew) {
|
1043
|
+
attrsArr[j] = _attr;
|
1044
|
+
newAttrs[_attr] = undefined;
|
1045
|
+
} else if (attrsArr[j] !== _attr) {
|
1046
|
+
break;
|
1047
|
+
}
|
1048
|
+
|
1049
|
+
var value = arguments[i + 1];
|
1050
|
+
if (isNew || attrsArr[j + 1] !== value) {
|
1051
|
+
attrsArr[j + 1] = value;
|
1052
|
+
updateAttribute(node, _attr, value);
|
1053
|
+
}
|
1054
|
+
}
|
1055
|
+
|
1056
|
+
if (i < arguments.length || j < attrsArr.length) {
|
1057
|
+
for (; i < arguments.length; i += 1, j += 1) {
|
1058
|
+
attrsArr[j] = arguments[i];
|
1059
|
+
}
|
1060
|
+
|
1061
|
+
if (j < attrsArr.length) {
|
1062
|
+
attrsArr.length = j;
|
1063
|
+
}
|
1064
|
+
|
1065
|
+
/*
|
1066
|
+
* Actually perform the attribute update.
|
1067
|
+
*/
|
1068
|
+
for (i = 0; i < attrsArr.length; i += 2) {
|
1069
|
+
var name = /** @type {string} */attrsArr[i];
|
1070
|
+
var value = attrsArr[i + 1];
|
1071
|
+
newAttrs[name] = value;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
for (var _attr2 in newAttrs) {
|
1075
|
+
updateAttribute(node, _attr2, newAttrs[_attr2]);
|
1076
|
+
newAttrs[_attr2] = undefined;
|
1077
|
+
}
|
1078
|
+
}
|
1079
|
+
|
1080
|
+
return node;
|
1081
|
+
};
|
1082
|
+
|
1083
|
+
/**
|
1084
|
+
* Declares a virtual Element at the current location in the document. This
|
1085
|
+
* corresponds to an opening tag and a elementClose tag is required. This is
|
1086
|
+
* like elementOpen, but the attributes are defined using the attr function
|
1087
|
+
* rather than being passed as arguments. Must be folllowed by 0 or more calls
|
1088
|
+
* to attr, then a call to elementOpenEnd.
|
1089
|
+
* @param {string} tag The element's tag.
|
1090
|
+
* @param {?string=} key The key used to identify this element. This can be an
|
1091
|
+
* empty string, but performance may be better if a unique value is used
|
1092
|
+
* when iterating over an array of items.
|
1093
|
+
* @param {?Array<*>=} statics An array of attribute name/value pairs of the
|
1094
|
+
* static attributes for the Element. These will only be set once when the
|
1095
|
+
* Element is created.
|
1096
|
+
*/
|
1097
|
+
var elementOpenStart = function (tag, key, statics) {
|
1098
|
+
if ('production' !== 'production') {}
|
1099
|
+
|
1100
|
+
argsBuilder[0] = tag;
|
1101
|
+
argsBuilder[1] = key;
|
1102
|
+
argsBuilder[2] = statics;
|
1103
|
+
};
|
1104
|
+
|
1105
|
+
/***
|
1106
|
+
* Defines a virtual attribute at this point of the DOM. This is only valid
|
1107
|
+
* when called between elementOpenStart and elementOpenEnd.
|
1108
|
+
*
|
1109
|
+
* @param {string} name
|
1110
|
+
* @param {*} value
|
1111
|
+
*/
|
1112
|
+
var attr = function (name, value) {
|
1113
|
+
if ('production' !== 'production') {}
|
1114
|
+
|
1115
|
+
argsBuilder.push(name);
|
1116
|
+
argsBuilder.push(value);
|
1117
|
+
};
|
1118
|
+
|
1119
|
+
/**
|
1120
|
+
* Closes an open tag started with elementOpenStart.
|
1121
|
+
* @return {!Element} The corresponding Element.
|
1122
|
+
*/
|
1123
|
+
var elementOpenEnd = function () {
|
1124
|
+
if ('production' !== 'production') {}
|
1125
|
+
|
1126
|
+
var node = elementOpen.apply(null, argsBuilder);
|
1127
|
+
argsBuilder.length = 0;
|
1128
|
+
return node;
|
1129
|
+
};
|
1130
|
+
|
1131
|
+
/**
|
1132
|
+
* Closes an open virtual Element.
|
1133
|
+
*
|
1134
|
+
* @param {string} tag The element's tag.
|
1135
|
+
* @return {!Element} The corresponding Element.
|
1136
|
+
*/
|
1137
|
+
var elementClose = function (tag) {
|
1138
|
+
if ('production' !== 'production') {}
|
1139
|
+
|
1140
|
+
var node = coreElementClose();
|
1141
|
+
|
1142
|
+
if ('production' !== 'production') {}
|
1143
|
+
|
1144
|
+
return node;
|
1145
|
+
};
|
1146
|
+
|
1147
|
+
/**
|
1148
|
+
* Declares a virtual Element at the current location in the document that has
|
1149
|
+
* no children.
|
1150
|
+
* @param {string} tag The element's tag.
|
1151
|
+
* @param {?string=} key The key used to identify this element. This can be an
|
1152
|
+
* empty string, but performance may be better if a unique value is used
|
1153
|
+
* when iterating over an array of items.
|
1154
|
+
* @param {?Array<*>=} statics An array of attribute name/value pairs of the
|
1155
|
+
* static attributes for the Element. These will only be set once when the
|
1156
|
+
* Element is created.
|
1157
|
+
* @param {...*} var_args Attribute name/value pairs of the dynamic attributes
|
1158
|
+
* for the Element.
|
1159
|
+
* @return {!Element} The corresponding Element.
|
1160
|
+
*/
|
1161
|
+
var elementVoid = function (tag, key, statics, var_args) {
|
1162
|
+
elementOpen.apply(null, arguments);
|
1163
|
+
return elementClose(tag);
|
1164
|
+
};
|
1165
|
+
|
1166
|
+
/**
|
1167
|
+
* Declares a virtual Text at this point in the document.
|
1168
|
+
*
|
1169
|
+
* @param {string|number|boolean} value The value of the Text.
|
1170
|
+
* @param {...(function((string|number|boolean)):string)} var_args
|
1171
|
+
* Functions to format the value which are called only when the value has
|
1172
|
+
* changed.
|
1173
|
+
* @return {!Text} The corresponding text node.
|
1174
|
+
*/
|
1175
|
+
var text = function (value, var_args) {
|
1176
|
+
if ('production' !== 'production') {}
|
1177
|
+
|
1178
|
+
var node = coreText();
|
1179
|
+
var data = getData(node);
|
1180
|
+
|
1181
|
+
if (data.text !== value) {
|
1182
|
+
data.text = /** @type {string} */value;
|
1183
|
+
|
1184
|
+
var formatted = value;
|
1185
|
+
for (var i = 1; i < arguments.length; i += 1) {
|
1186
|
+
/*
|
1187
|
+
* Call the formatter function directly to prevent leaking arguments.
|
1188
|
+
* https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
|
1189
|
+
*/
|
1190
|
+
var fn = arguments[i];
|
1191
|
+
formatted = fn(formatted);
|
1192
|
+
}
|
1193
|
+
|
1194
|
+
node.data = formatted;
|
1195
|
+
}
|
1196
|
+
|
1197
|
+
return node;
|
1198
|
+
};
|
1199
|
+
|
1200
|
+
exports.patch = patchInner;
|
1201
|
+
exports.patchInner = patchInner;
|
1202
|
+
exports.patchOuter = patchOuter;
|
1203
|
+
exports.currentElement = currentElement;
|
1204
|
+
exports.currentPointer = currentPointer;
|
1205
|
+
exports.skip = skip;
|
1206
|
+
exports.skipNode = skipNode;
|
1207
|
+
exports.elementVoid = elementVoid;
|
1208
|
+
exports.elementOpenStart = elementOpenStart;
|
1209
|
+
exports.elementOpenEnd = elementOpenEnd;
|
1210
|
+
exports.elementOpen = elementOpen;
|
1211
|
+
exports.elementClose = elementClose;
|
1212
|
+
exports.text = text;
|
1213
|
+
exports.attr = attr;
|
1214
|
+
exports.symbols = symbols;
|
1215
|
+
exports.attributes = attributes;
|
1216
|
+
exports.applyAttr = applyAttr;
|
1217
|
+
exports.applyProp = applyProp;
|
1218
|
+
exports.notifications = notifications;
|
1219
|
+
exports.importNode = importNode;
|
1220
|
+
|
1221
|
+
}));
|
1222
|
+
|
1223
|
+
//!#! sourceMappingURL=https://ajax.googleapis.com/ajax/libs/incrementaldom/0.5.1/incremental-dom.js.map
|