@ckeditor/ckeditor5-ui 39.0.1 → 40.0.0

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.
Files changed (229) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +4 -4
  4. package/lang/contexts.json +5 -1
  5. package/lang/translations/ar.po +17 -0
  6. package/lang/translations/ast.po +17 -0
  7. package/lang/translations/az.po +17 -0
  8. package/lang/translations/bg.po +17 -0
  9. package/lang/translations/bn.po +17 -0
  10. package/lang/translations/ca.po +17 -0
  11. package/lang/translations/cs.po +17 -0
  12. package/lang/translations/da.po +17 -0
  13. package/lang/translations/de-ch.po +17 -0
  14. package/lang/translations/de.po +17 -0
  15. package/lang/translations/el.po +17 -0
  16. package/lang/translations/en-au.po +17 -0
  17. package/lang/translations/en-gb.po +17 -0
  18. package/lang/translations/en.po +17 -0
  19. package/lang/translations/eo.po +17 -0
  20. package/lang/translations/es.po +17 -0
  21. package/lang/translations/et.po +17 -0
  22. package/lang/translations/eu.po +17 -0
  23. package/lang/translations/fa.po +17 -0
  24. package/lang/translations/fi.po +17 -0
  25. package/lang/translations/fr.po +17 -0
  26. package/lang/translations/gl.po +17 -0
  27. package/lang/translations/he.po +17 -0
  28. package/lang/translations/hi.po +17 -0
  29. package/lang/translations/hr.po +17 -0
  30. package/lang/translations/hu.po +17 -0
  31. package/lang/translations/id.po +17 -0
  32. package/lang/translations/it.po +17 -0
  33. package/lang/translations/ja.po +17 -0
  34. package/lang/translations/km.po +17 -0
  35. package/lang/translations/kn.po +17 -0
  36. package/lang/translations/ko.po +17 -0
  37. package/lang/translations/ku.po +17 -0
  38. package/lang/translations/lt.po +17 -0
  39. package/lang/translations/lv.po +17 -0
  40. package/lang/translations/ms.po +17 -0
  41. package/lang/translations/nb.po +17 -0
  42. package/lang/translations/ne.po +17 -0
  43. package/lang/translations/nl.po +17 -0
  44. package/lang/translations/no.po +17 -0
  45. package/lang/translations/pl.po +17 -0
  46. package/lang/translations/pt-br.po +17 -0
  47. package/lang/translations/pt.po +17 -0
  48. package/lang/translations/ro.po +17 -0
  49. package/lang/translations/ru.po +17 -0
  50. package/lang/translations/sk.po +17 -0
  51. package/lang/translations/sl.po +17 -0
  52. package/lang/translations/sq.po +17 -0
  53. package/lang/translations/sr-latn.po +17 -0
  54. package/lang/translations/sr.po +17 -0
  55. package/lang/translations/sv.po +17 -0
  56. package/lang/translations/th.po +17 -0
  57. package/lang/translations/tk.po +17 -0
  58. package/lang/translations/tr.po +17 -0
  59. package/lang/translations/tt.po +17 -0
  60. package/lang/translations/ug.po +17 -0
  61. package/lang/translations/uk.po +17 -0
  62. package/lang/translations/ur.po +17 -0
  63. package/lang/translations/uz.po +17 -0
  64. package/lang/translations/vi.po +17 -0
  65. package/lang/translations/zh-cn.po +17 -0
  66. package/lang/translations/zh.po +17 -0
  67. package/package.json +3 -7
  68. package/src/augmentation.d.ts +86 -86
  69. package/src/augmentation.js +5 -5
  70. package/src/autocomplete/autocompleteview.d.ts +81 -0
  71. package/src/autocomplete/autocompleteview.js +146 -0
  72. package/src/bindings/addkeyboardhandlingforgrid.d.ts +27 -27
  73. package/src/bindings/addkeyboardhandlingforgrid.js +107 -107
  74. package/src/bindings/clickoutsidehandler.d.ts +28 -28
  75. package/src/bindings/clickoutsidehandler.js +36 -36
  76. package/src/bindings/csstransitiondisablermixin.d.ts +40 -40
  77. package/src/bindings/csstransitiondisablermixin.js +55 -55
  78. package/src/bindings/injectcsstransitiondisabler.d.ts +59 -59
  79. package/src/bindings/injectcsstransitiondisabler.js +71 -71
  80. package/src/bindings/preventdefault.d.ts +33 -33
  81. package/src/bindings/preventdefault.js +34 -34
  82. package/src/bindings/submithandler.d.ts +57 -57
  83. package/src/bindings/submithandler.js +47 -47
  84. package/src/button/button.d.ts +178 -178
  85. package/src/button/button.js +5 -5
  86. package/src/button/buttonlabel.d.ts +34 -0
  87. package/src/button/buttonlabel.js +5 -0
  88. package/src/button/buttonlabelview.d.ts +31 -0
  89. package/src/button/buttonlabelview.js +42 -0
  90. package/src/button/buttonview.d.ts +185 -177
  91. package/src/button/buttonview.js +219 -231
  92. package/src/button/switchbuttonview.d.ts +45 -45
  93. package/src/button/switchbuttonview.js +75 -75
  94. package/src/colorgrid/colorgridview.d.ts +132 -132
  95. package/src/colorgrid/colorgridview.js +124 -124
  96. package/src/colorgrid/colortileview.d.ts +28 -28
  97. package/src/colorgrid/colortileview.js +40 -40
  98. package/src/colorgrid/utils.d.ts +47 -47
  99. package/src/colorgrid/utils.js +84 -84
  100. package/src/colorpicker/colorpickerview.d.ts +137 -137
  101. package/src/colorpicker/colorpickerview.js +270 -270
  102. package/src/colorpicker/utils.d.ts +43 -43
  103. package/src/colorpicker/utils.js +99 -99
  104. package/src/colorselector/colorgridsfragmentview.d.ts +194 -194
  105. package/src/colorselector/colorgridsfragmentview.js +289 -289
  106. package/src/colorselector/colorpickerfragmentview.d.ts +128 -128
  107. package/src/colorselector/colorpickerfragmentview.js +205 -205
  108. package/src/colorselector/colorselectorview.d.ts +242 -242
  109. package/src/colorselector/colorselectorview.js +256 -256
  110. package/src/colorselector/documentcolorcollection.d.ts +70 -70
  111. package/src/colorselector/documentcolorcollection.js +42 -42
  112. package/src/componentfactory.d.ts +81 -81
  113. package/src/componentfactory.js +104 -104
  114. package/src/dropdown/button/dropdownbutton.d.ts +25 -25
  115. package/src/dropdown/button/dropdownbutton.js +5 -5
  116. package/src/dropdown/button/dropdownbuttonview.d.ts +48 -48
  117. package/src/dropdown/button/dropdownbuttonview.js +66 -66
  118. package/src/dropdown/button/splitbuttonview.d.ts +161 -161
  119. package/src/dropdown/button/splitbuttonview.js +152 -152
  120. package/src/dropdown/dropdownpanelfocusable.d.ts +21 -21
  121. package/src/dropdown/dropdownpanelfocusable.js +5 -5
  122. package/src/dropdown/dropdownpanelview.d.ts +62 -62
  123. package/src/dropdown/dropdownpanelview.js +97 -97
  124. package/src/dropdown/dropdownview.d.ts +315 -315
  125. package/src/dropdown/dropdownview.js +379 -378
  126. package/src/dropdown/utils.d.ts +235 -221
  127. package/src/dropdown/utils.js +458 -437
  128. package/src/editableui/editableuiview.d.ts +72 -72
  129. package/src/editableui/editableuiview.js +112 -112
  130. package/src/editableui/inline/inlineeditableuiview.d.ts +40 -40
  131. package/src/editableui/inline/inlineeditableuiview.js +48 -48
  132. package/src/editorui/bodycollection.d.ts +55 -55
  133. package/src/editorui/bodycollection.js +84 -84
  134. package/src/editorui/boxed/boxededitoruiview.d.ts +40 -40
  135. package/src/editorui/boxed/boxededitoruiview.js +81 -81
  136. package/src/editorui/editorui.d.ts +282 -282
  137. package/src/editorui/editorui.js +410 -410
  138. package/src/editorui/editoruiview.d.ts +39 -39
  139. package/src/editorui/editoruiview.js +38 -38
  140. package/src/editorui/poweredby.d.ts +71 -71
  141. package/src/editorui/poweredby.js +276 -299
  142. package/src/focuscycler.d.ts +226 -183
  143. package/src/focuscycler.js +245 -220
  144. package/src/formheader/formheaderview.d.ts +59 -53
  145. package/src/formheader/formheaderview.js +69 -63
  146. package/src/highlightedtext/highlightedtextview.d.ts +38 -0
  147. package/src/highlightedtext/highlightedtextview.js +102 -0
  148. package/src/icon/iconview.d.ts +85 -78
  149. package/src/icon/iconview.js +114 -112
  150. package/src/iframe/iframeview.d.ts +50 -50
  151. package/src/iframe/iframeview.js +63 -63
  152. package/src/index.d.ts +73 -63
  153. package/src/index.js +70 -62
  154. package/src/input/inputbase.d.ts +107 -0
  155. package/src/input/inputbase.js +110 -0
  156. package/src/input/inputview.d.ts +36 -121
  157. package/src/input/inputview.js +24 -106
  158. package/src/inputnumber/inputnumberview.d.ts +49 -49
  159. package/src/inputnumber/inputnumberview.js +40 -40
  160. package/src/inputtext/inputtextview.d.ts +18 -18
  161. package/src/inputtext/inputtextview.js +27 -27
  162. package/src/label/labelview.d.ts +36 -36
  163. package/src/label/labelview.js +41 -41
  164. package/src/labeledfield/labeledfieldview.d.ts +187 -182
  165. package/src/labeledfield/labeledfieldview.js +157 -157
  166. package/src/labeledfield/utils.d.ts +123 -93
  167. package/src/labeledfield/utils.js +176 -131
  168. package/src/labeledinput/labeledinputview.d.ts +125 -125
  169. package/src/labeledinput/labeledinputview.js +125 -125
  170. package/src/list/listitemgroupview.d.ts +51 -0
  171. package/src/list/listitemgroupview.js +75 -0
  172. package/src/list/listitemview.d.ts +36 -35
  173. package/src/list/listitemview.js +42 -40
  174. package/src/list/listseparatorview.d.ts +18 -18
  175. package/src/list/listseparatorview.js +28 -28
  176. package/src/list/listview.d.ts +122 -65
  177. package/src/list/listview.js +187 -90
  178. package/src/model.d.ts +22 -22
  179. package/src/model.js +31 -31
  180. package/src/notification/notification.d.ts +211 -211
  181. package/src/notification/notification.js +187 -187
  182. package/src/panel/balloon/balloonpanelview.d.ts +685 -685
  183. package/src/panel/balloon/balloonpanelview.js +1010 -988
  184. package/src/panel/balloon/contextualballoon.d.ts +299 -299
  185. package/src/panel/balloon/contextualballoon.js +572 -572
  186. package/src/panel/sticky/stickypanelview.d.ts +156 -158
  187. package/src/panel/sticky/stickypanelview.js +234 -231
  188. package/src/search/filteredview.d.ts +31 -0
  189. package/src/search/filteredview.js +5 -0
  190. package/src/search/searchinfoview.d.ts +45 -0
  191. package/src/search/searchinfoview.js +59 -0
  192. package/src/search/searchresultsview.d.ts +54 -0
  193. package/src/search/searchresultsview.js +65 -0
  194. package/src/search/text/searchtextqueryview.d.ts +76 -0
  195. package/src/search/text/searchtextqueryview.js +75 -0
  196. package/src/search/text/searchtextview.d.ts +219 -0
  197. package/src/search/text/searchtextview.js +201 -0
  198. package/src/spinner/spinnerview.d.ts +25 -0
  199. package/src/spinner/spinnerview.js +38 -0
  200. package/src/template.d.ts +942 -942
  201. package/src/template.js +1294 -1294
  202. package/src/textarea/textareaview.d.ts +88 -0
  203. package/src/textarea/textareaview.js +140 -0
  204. package/src/toolbar/balloon/balloontoolbar.d.ts +122 -122
  205. package/src/toolbar/balloon/balloontoolbar.js +300 -300
  206. package/src/toolbar/block/blockbuttonview.d.ts +35 -35
  207. package/src/toolbar/block/blockbuttonview.js +41 -41
  208. package/src/toolbar/block/blocktoolbar.d.ts +161 -161
  209. package/src/toolbar/block/blocktoolbar.js +395 -391
  210. package/src/toolbar/normalizetoolbarconfig.d.ts +40 -39
  211. package/src/toolbar/normalizetoolbarconfig.js +51 -51
  212. package/src/toolbar/toolbarlinebreakview.d.ts +18 -18
  213. package/src/toolbar/toolbarlinebreakview.js +28 -28
  214. package/src/toolbar/toolbarseparatorview.d.ts +18 -18
  215. package/src/toolbar/toolbarseparatorview.js +28 -28
  216. package/src/toolbar/toolbarview.d.ts +266 -265
  217. package/src/toolbar/toolbarview.js +719 -717
  218. package/src/tooltipmanager.d.ts +180 -180
  219. package/src/tooltipmanager.js +353 -353
  220. package/src/view.d.ts +422 -422
  221. package/src/view.js +396 -396
  222. package/src/viewcollection.d.ts +139 -139
  223. package/src/viewcollection.js +206 -206
  224. package/theme/components/autocomplete/autocomplete.css +22 -0
  225. package/theme/components/formheader/formheader.css +8 -0
  226. package/theme/components/highlightedtext/highlightedtext.css +12 -0
  227. package/theme/components/search/search.css +43 -0
  228. package/theme/components/spinner/spinner.css +23 -0
  229. package/theme/components/textarea/textarea.css +10 -0
package/src/template.js CHANGED
@@ -1,1294 +1,1294 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module ui/template
7
- */
8
- /* global document */
9
- import View from './view';
10
- import ViewCollection from './viewcollection';
11
- import { CKEditorError, EmitterMixin, isNode, toArray } from '@ckeditor/ckeditor5-utils';
12
- import { isObject, cloneDeepWith } from 'lodash-es';
13
- const xhtmlNs = 'http://www.w3.org/1999/xhtml';
14
- /**
15
- * A basic Template class. It renders a DOM HTML element or text from a
16
- * {@link module:ui/template~TemplateDefinition definition} and supports element attributes, children,
17
- * bindings to {@link module:utils/observablemixin~Observable observables} and DOM event propagation.
18
- *
19
- * A simple template can look like this:
20
- *
21
- * ```ts
22
- * const bind = Template.bind( observable, emitter );
23
- *
24
- * new Template( {
25
- * tag: 'p',
26
- * attributes: {
27
- * class: 'foo',
28
- * style: {
29
- * backgroundColor: 'yellow'
30
- * }
31
- * },
32
- * on: {
33
- * click: bind.to( 'clicked' )
34
- * },
35
- * children: [
36
- * 'A paragraph.'
37
- * ]
38
- * } ).render();
39
- * ```
40
- *
41
- * and it will render the following HTML element:
42
- *
43
- * ```html
44
- * <p class="foo" style="background-color: yellow;">A paragraph.</p>
45
- * ```
46
- *
47
- * Additionally, the `observable` will always fire `clicked` upon clicking `<p>` in the DOM.
48
- *
49
- * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex
50
- * template definitions.
51
- */
52
- export default class Template extends EmitterMixin() {
53
- /**
54
- * Creates an instance of the {@link ~Template} class.
55
- *
56
- * @param def The definition of the template.
57
- */
58
- constructor(def) {
59
- super();
60
- Object.assign(this, normalize(clone(def)));
61
- this._isRendered = false;
62
- this._revertData = null;
63
- }
64
- /**
65
- * Renders a DOM Node (an HTML element or text) out of the template.
66
- *
67
- * ```ts
68
- * const domNode = new Template( { ... } ).render();
69
- * ```
70
- *
71
- * See: {@link #apply}.
72
- */
73
- render() {
74
- const node = this._renderNode({
75
- intoFragment: true
76
- });
77
- this._isRendered = true;
78
- return node;
79
- }
80
- /**
81
- * Applies the template to an existing DOM Node, either HTML element or text.
82
- *
83
- * **Note:** No new DOM nodes will be created. Applying extends:
84
- *
85
- * {@link module:ui/template~TemplateDefinition attributes},
86
- * {@link module:ui/template~TemplateDefinition event listeners}, and
87
- * `textContent` of {@link module:ui/template~TemplateDefinition children} only.
88
- *
89
- * **Note:** Existing `class` and `style` attributes are extended when a template
90
- * is applied to an HTML element, while other attributes and `textContent` are overridden.
91
- *
92
- * **Note:** The process of applying a template can be easily reverted using the
93
- * {@link module:ui/template~Template#revert} method.
94
- *
95
- * ```ts
96
- * const element = document.createElement( 'div' );
97
- * const observable = new Model( { divClass: 'my-div' } );
98
- * const emitter = Object.create( EmitterMixin );
99
- * const bind = Template.bind( observable, emitter );
100
- *
101
- * new Template( {
102
- * attributes: {
103
- * id: 'first-div',
104
- * class: bind.to( 'divClass' )
105
- * },
106
- * on: {
107
- * click: bind( 'elementClicked' ) // Will be fired by the observable.
108
- * },
109
- * children: [
110
- * 'Div text.'
111
- * ]
112
- * } ).apply( element );
113
- *
114
- * console.log( element.outerHTML ); // -> '<div id="first-div" class="my-div"></div>'
115
- * ```
116
- *
117
- * @see module:ui/template~Template#render
118
- * @see module:ui/template~Template#revert
119
- * @param node Root node for the template to apply.
120
- */
121
- apply(node) {
122
- this._revertData = getEmptyRevertData();
123
- this._renderNode({
124
- node,
125
- intoFragment: false,
126
- isApplying: true,
127
- revertData: this._revertData
128
- });
129
- return node;
130
- }
131
- /**
132
- * Reverts a template {@link module:ui/template~Template#apply applied} to a DOM node.
133
- *
134
- * @param node The root node for the template to revert. In most of the cases, it is the
135
- * same node used by {@link module:ui/template~Template#apply}.
136
- */
137
- revert(node) {
138
- if (!this._revertData) {
139
- /**
140
- * Attempting to revert a template which has not been applied yet.
141
- *
142
- * @error ui-template-revert-not-applied
143
- */
144
- throw new CKEditorError('ui-template-revert-not-applied', [this, node]);
145
- }
146
- this._revertTemplateFromNode(node, this._revertData);
147
- }
148
- /**
149
- * Returns an iterator which traverses the template in search of {@link module:ui/view~View}
150
- * instances and returns them one by one.
151
- *
152
- * ```ts
153
- * const viewFoo = new View();
154
- * const viewBar = new View();
155
- * const viewBaz = new View();
156
- * const template = new Template( {
157
- * tag: 'div',
158
- * children: [
159
- * viewFoo,
160
- * {
161
- * tag: 'div',
162
- * children: [
163
- * viewBar
164
- * ]
165
- * },
166
- * viewBaz
167
- * ]
168
- * } );
169
- *
170
- * // Logs: viewFoo, viewBar, viewBaz
171
- * for ( const view of template.getViews() ) {
172
- * console.log( view );
173
- * }
174
- * ```
175
- */
176
- *getViews() {
177
- function* search(def) {
178
- if (def.children) {
179
- for (const child of def.children) {
180
- if (isView(child)) {
181
- yield child;
182
- }
183
- else if (isTemplate(child)) {
184
- yield* search(child);
185
- }
186
- }
187
- }
188
- }
189
- yield* search(this);
190
- }
191
- /**
192
- * An entry point to the interface which binds DOM nodes to
193
- * {@link module:utils/observablemixin~Observable observables}.
194
- * There are two types of bindings:
195
- *
196
- * * HTML element attributes or text `textContent` synchronized with attributes of an
197
- * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}
198
- * and {@link module:ui/template~BindChain#if}.
199
- *
200
- * ```ts
201
- * const bind = Template.bind( observable, emitter );
202
- *
203
- * new Template( {
204
- * attributes: {
205
- * // Binds the element "class" attribute to observable#classAttribute.
206
- * class: bind.to( 'classAttribute' )
207
- * }
208
- * } ).render();
209
- * ```
210
- *
211
- * * DOM events fired on HTML element propagated through
212
- * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}.
213
- *
214
- * ```ts
215
- * const bind = Template.bind( observable, emitter );
216
- *
217
- * new Template( {
218
- * on: {
219
- * // Will be fired by the observable.
220
- * click: bind( 'elementClicked' )
221
- * }
222
- * } ).render();
223
- * ```
224
- *
225
- * Also see {@link module:ui/view~View#bindTemplate}.
226
- *
227
- * @param observable An observable which provides boundable attributes.
228
- * @param emitter An emitter that listens to observable attribute
229
- * changes or DOM Events (depending on the kind of the binding). Usually, a {@link module:ui/view~View} instance.
230
- */
231
- static bind(observable, emitter) {
232
- return {
233
- to(eventNameOrFunctionOrAttribute, callback) {
234
- return new TemplateToBinding({
235
- eventNameOrFunction: eventNameOrFunctionOrAttribute,
236
- attribute: eventNameOrFunctionOrAttribute,
237
- observable, emitter, callback
238
- });
239
- },
240
- if(attribute, valueIfTrue, callback) {
241
- return new TemplateIfBinding({
242
- observable, emitter, attribute, valueIfTrue, callback
243
- });
244
- }
245
- };
246
- }
247
- /**
248
- * Extends an existing {@link module:ui/template~Template} instance with some additional content
249
- * from another {@link module:ui/template~TemplateDefinition}.
250
- *
251
- * ```ts
252
- * const bind = Template.bind( observable, emitter );
253
- *
254
- * const template = new Template( {
255
- * tag: 'p',
256
- * attributes: {
257
- * class: 'a',
258
- * data-x: bind.to( 'foo' )
259
- * },
260
- * children: [
261
- * {
262
- * tag: 'span',
263
- * attributes: {
264
- * class: 'b'
265
- * },
266
- * children: [
267
- * 'Span'
268
- * ]
269
- * }
270
- * ]
271
- * } );
272
- *
273
- * // Instance-level extension.
274
- * Template.extend( template, {
275
- * attributes: {
276
- * class: 'b',
277
- * data-x: bind.to( 'bar' )
278
- * },
279
- * children: [
280
- * {
281
- * attributes: {
282
- * class: 'c'
283
- * }
284
- * }
285
- * ]
286
- * } );
287
- *
288
- * // Child extension.
289
- * Template.extend( template.children[ 0 ], {
290
- * attributes: {
291
- * class: 'd'
292
- * }
293
- * } );
294
- * ```
295
- *
296
- * the `outerHTML` of `template.render()` is:
297
- *
298
- * ```html
299
- * <p class="a b" data-x="{ observable.foo } { observable.bar }">
300
- * <span class="b c d">Span</span>
301
- * </p>
302
- * ```
303
- *
304
- * @param template An existing template instance to be extended.
305
- * @param def Additional definition to be applied to a template.
306
- */
307
- static extend(template, def) {
308
- if (template._isRendered) {
309
- /**
310
- * Extending a template after rendering may not work as expected. To make sure
311
- * the {@link module:ui/template~Template.extend extending} works for an element,
312
- * make sure it happens before {@link module:ui/template~Template#render} is called.
313
- *
314
- * @error template-extend-render
315
- */
316
- throw new CKEditorError('template-extend-render', [this, template]);
317
- }
318
- extendTemplate(template, normalize(clone(def)));
319
- }
320
- /**
321
- * Renders a DOM Node (either an HTML element or text) out of the template.
322
- *
323
- * @param data Rendering data.
324
- */
325
- _renderNode(data) {
326
- let isInvalid;
327
- if (data.node) {
328
- // When applying, a definition cannot have "tag" and "text" at the same time.
329
- isInvalid = this.tag && this.text;
330
- }
331
- else {
332
- // When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
333
- isInvalid = this.tag ? this.text : !this.text;
334
- }
335
- if (isInvalid) {
336
- /**
337
- * Node definition cannot have the "tag" and "text" properties at the same time.
338
- * Node definition must have either "tag" or "text" when rendering a new Node.
339
- *
340
- * @error ui-template-wrong-syntax
341
- */
342
- throw new CKEditorError('ui-template-wrong-syntax', this);
343
- }
344
- if (this.text) {
345
- return this._renderText(data);
346
- }
347
- else {
348
- return this._renderElement(data);
349
- }
350
- }
351
- /**
352
- * Renders an HTML element out of the template.
353
- *
354
- * @param data Rendering data.
355
- */
356
- _renderElement(data) {
357
- let node = data.node;
358
- if (!node) {
359
- node = data.node = document.createElementNS(this.ns || xhtmlNs, this.tag);
360
- }
361
- this._renderAttributes(data);
362
- this._renderElementChildren(data);
363
- this._setUpListeners(data);
364
- return node;
365
- }
366
- /**
367
- * Renders a text node out of {@link module:ui/template~Template#text}.
368
- *
369
- * @param data Rendering data.
370
- */
371
- _renderText(data) {
372
- let node = data.node;
373
- // Save the original textContent to revert it in #revert().
374
- if (node) {
375
- data.revertData.text = node.textContent;
376
- }
377
- else {
378
- node = data.node = document.createTextNode('');
379
- }
380
- // Check if this Text Node is bound to Observable. Cases:
381
- //
382
- // text: [ Template.bind( ... ).to( ... ) ]
383
- //
384
- // text: [
385
- // 'foo',
386
- // Template.bind( ... ).to( ... ),
387
- // ...
388
- // ]
389
- //
390
- if (hasTemplateBinding(this.text)) {
391
- this._bindToObservable({
392
- schema: this.text,
393
- updater: getTextUpdater(node),
394
- data
395
- });
396
- }
397
- // Simply set text. Cases:
398
- //
399
- // text: [ 'all', 'are', 'static' ]
400
- //
401
- // text: [ 'foo' ]
402
- //
403
- else {
404
- node.textContent = this.text.join('');
405
- }
406
- return node;
407
- }
408
- /**
409
- * Renders HTML element attributes out of {@link module:ui/template~Template#attributes}.
410
- *
411
- * @param data Rendering data.
412
- */
413
- _renderAttributes(data) {
414
- if (!this.attributes) {
415
- return;
416
- }
417
- const node = data.node;
418
- const revertData = data.revertData;
419
- for (const attrName in this.attributes) {
420
- // Current attribute value in DOM.
421
- const domAttrValue = node.getAttribute(attrName);
422
- // The value to be set.
423
- const attrValue = this.attributes[attrName];
424
- // Save revert data.
425
- if (revertData) {
426
- revertData.attributes[attrName] = domAttrValue;
427
- }
428
- // Detect custom namespace:
429
- //
430
- // class: {
431
- // ns: 'abc',
432
- // value: Template.bind( ... ).to( ... )
433
- // }
434
- //
435
- const attrNs = isNamespaced(attrValue) ? attrValue[0].ns : null;
436
- // Activate binding if one is found. Cases:
437
- //
438
- // class: [
439
- // Template.bind( ... ).to( ... )
440
- // ]
441
- //
442
- // class: [
443
- // 'bar',
444
- // Template.bind( ... ).to( ... ),
445
- // 'baz'
446
- // ]
447
- //
448
- // class: {
449
- // ns: 'abc',
450
- // value: Template.bind( ... ).to( ... )
451
- // }
452
- //
453
- if (hasTemplateBinding(attrValue)) {
454
- // Normalize attributes with additional data like namespace:
455
- //
456
- // class: {
457
- // ns: 'abc',
458
- // value: [ ... ]
459
- // }
460
- //
461
- const valueToBind = isNamespaced(attrValue) ? attrValue[0].value : attrValue;
462
- // Extend the original value of attributes like "style" and "class",
463
- // don't override them.
464
- if (revertData && shouldExtend(attrName)) {
465
- valueToBind.unshift(domAttrValue);
466
- }
467
- this._bindToObservable({
468
- schema: valueToBind,
469
- updater: getAttributeUpdater(node, attrName, attrNs),
470
- data
471
- });
472
- }
473
- // Style attribute could be an Object so it needs to be parsed in a specific way.
474
- //
475
- // style: {
476
- // width: '100px',
477
- // height: Template.bind( ... ).to( ... )
478
- // }
479
- //
480
- else if (attrName == 'style' && typeof attrValue[0] !== 'string') {
481
- this._renderStyleAttribute(attrValue[0], data);
482
- }
483
- // Otherwise simply set the static attribute:
484
- //
485
- // class: [ 'foo' ]
486
- //
487
- // class: [ 'all', 'are', 'static' ]
488
- //
489
- // class: [
490
- // {
491
- // ns: 'abc',
492
- // value: [ 'foo' ]
493
- // }
494
- // ]
495
- //
496
- else {
497
- // Extend the original value of attributes like "style" and "class",
498
- // don't override them.
499
- if (revertData && domAttrValue && shouldExtend(attrName)) {
500
- attrValue.unshift(domAttrValue);
501
- }
502
- const value = attrValue
503
- // Retrieve "values" from:
504
- //
505
- // class: [
506
- // {
507
- // ns: 'abc',
508
- // value: [ ... ]
509
- // }
510
- // ]
511
- //
512
- .map((val) => val ? (val.value || val) : val)
513
- // Flatten the array.
514
- .reduce((prev, next) => prev.concat(next), [])
515
- // Convert into string.
516
- .reduce(arrayValueReducer, '');
517
- if (!isFalsy(value)) {
518
- node.setAttributeNS(attrNs, attrName, value);
519
- }
520
- }
521
- }
522
- }
523
- /**
524
- * Renders the `style` attribute of an HTML element based on
525
- * {@link module:ui/template~Template#attributes}.
526
- *
527
- * A style attribute is an object with static values:
528
- *
529
- * ```ts
530
- * attributes: {
531
- * style: {
532
- * color: 'red'
533
- * }
534
- * }
535
- * ```
536
- *
537
- * or values bound to {@link module:ui/model~Model} properties:
538
- *
539
- * ```ts
540
- * attributes: {
541
- * style: {
542
- * color: bind.to( ... )
543
- * }
544
- * }
545
- * ```
546
- *
547
- * Note: The `style` attribute is rendered without setting the namespace. It does not seem to be
548
- * needed.
549
- *
550
- * @param styles Styles located in `attributes.style` of {@link module:ui/template~TemplateDefinition}.
551
- * @param data Rendering data.
552
- */
553
- _renderStyleAttribute(styles, data) {
554
- const node = data.node;
555
- for (const styleName in styles) {
556
- const styleValue = styles[styleName];
557
- // Cases:
558
- //
559
- // style: {
560
- // color: bind.to( 'attribute' )
561
- // }
562
- //
563
- if (hasTemplateBinding(styleValue)) {
564
- this._bindToObservable({
565
- schema: [styleValue],
566
- updater: getStyleUpdater(node, styleName),
567
- data
568
- });
569
- }
570
- // Cases:
571
- //
572
- // style: {
573
- // color: 'red'
574
- // }
575
- //
576
- else {
577
- node.style[styleName] = styleValue;
578
- }
579
- }
580
- }
581
- /**
582
- * Recursively renders HTML element's children from {@link module:ui/template~Template#children}.
583
- *
584
- * @param data Rendering data.
585
- */
586
- _renderElementChildren(data) {
587
- const node = data.node;
588
- const container = data.intoFragment ? document.createDocumentFragment() : node;
589
- const isApplying = data.isApplying;
590
- let childIndex = 0;
591
- for (const child of this.children) {
592
- if (isViewCollection(child)) {
593
- if (!isApplying) {
594
- child.setParent(node);
595
- // Note: ViewCollection renders its children.
596
- for (const view of child) {
597
- container.appendChild(view.element);
598
- }
599
- }
600
- }
601
- else if (isView(child)) {
602
- if (!isApplying) {
603
- if (!child.isRendered) {
604
- child.render();
605
- }
606
- container.appendChild(child.element);
607
- }
608
- }
609
- else if (isNode(child)) {
610
- container.appendChild(child);
611
- }
612
- else {
613
- if (isApplying) {
614
- const revertData = data.revertData;
615
- const childRevertData = getEmptyRevertData();
616
- revertData.children.push(childRevertData);
617
- child._renderNode({
618
- intoFragment: false,
619
- node: container.childNodes[childIndex++],
620
- isApplying: true,
621
- revertData: childRevertData
622
- });
623
- }
624
- else {
625
- container.appendChild(child.render());
626
- }
627
- }
628
- }
629
- if (data.intoFragment) {
630
- node.appendChild(container);
631
- }
632
- }
633
- /**
634
- * Activates `on` event listeners from the {@link module:ui/template~TemplateDefinition}
635
- * on an HTML element.
636
- *
637
- * @param data Rendering data.
638
- */
639
- _setUpListeners(data) {
640
- if (!this.eventListeners) {
641
- return;
642
- }
643
- for (const key in this.eventListeners) {
644
- const revertBindings = this.eventListeners[key].map(schemaItem => {
645
- const [domEvtName, domSelector] = key.split('@');
646
- return schemaItem.activateDomEventListener(domEvtName, domSelector, data);
647
- });
648
- if (data.revertData) {
649
- data.revertData.bindings.push(revertBindings);
650
- }
651
- }
652
- }
653
- /**
654
- * For a given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding}
655
- * activates the binding and sets its initial value.
656
- *
657
- * Note: {@link module:ui/template~TemplateValueSchema} can be for HTML element attributes or
658
- * text node `textContent`.
659
- *
660
- * @param options Binding options.
661
- * @param options.updater A function which updates the DOM (like attribute or text).
662
- * @param options.data Rendering data.
663
- */
664
- _bindToObservable({ schema, updater, data }) {
665
- const revertData = data.revertData;
666
- // Set initial values.
667
- syncValueSchemaValue(schema, updater, data);
668
- const revertBindings = schema
669
- // Filter "falsy" (false, undefined, null, '') value schema components out.
670
- .filter(item => !isFalsy(item))
671
- // Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
672
- .filter((item) => item.observable)
673
- // Once only the actual binding are left, let the emitter listen to observable change:attribute event.
674
- // TODO: Reduce the number of listeners attached as many bindings may listen
675
- // to the same observable attribute.
676
- .map(templateBinding => templateBinding.activateAttributeListener(schema, updater, data));
677
- if (revertData) {
678
- revertData.bindings.push(revertBindings);
679
- }
680
- }
681
- /**
682
- * Reverts {@link module:ui/template~RenderData#revertData template data} from a node to
683
- * return it to the original state.
684
- *
685
- * @param node A node to be reverted.
686
- * @param revertData An object that stores information about what changes have been made by
687
- * {@link #apply} to the node. See {@link module:ui/template~RenderData#revertData} for more information.
688
- */
689
- _revertTemplateFromNode(node, revertData) {
690
- for (const binding of revertData.bindings) {
691
- // Each binding may consist of several observable+observable#attribute.
692
- // like the following has 2:
693
- //
694
- // class: [
695
- // 'x',
696
- // bind.to( 'foo' ),
697
- // 'y',
698
- // bind.to( 'bar' )
699
- // ]
700
- //
701
- for (const revertBinding of binding) {
702
- revertBinding();
703
- }
704
- }
705
- if (revertData.text) {
706
- node.textContent = revertData.text;
707
- return;
708
- }
709
- const element = node;
710
- for (const attrName in revertData.attributes) {
711
- const attrValue = revertData.attributes[attrName];
712
- // When the attribute has **not** been set before #apply().
713
- if (attrValue === null) {
714
- element.removeAttribute(attrName);
715
- }
716
- else {
717
- element.setAttribute(attrName, attrValue);
718
- }
719
- }
720
- for (let i = 0; i < revertData.children.length; ++i) {
721
- this._revertTemplateFromNode(element.childNodes[i], revertData.children[i]);
722
- }
723
- }
724
- }
725
- /**
726
- * Describes a binding created by the {@link module:ui/template~Template.bind} interface.
727
- *
728
- * @internal
729
- */
730
- export class TemplateBinding {
731
- /**
732
- * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
733
- *
734
- * @param def The definition of the binding.
735
- */
736
- constructor(def) {
737
- this.attribute = def.attribute;
738
- this.observable = def.observable;
739
- this.emitter = def.emitter;
740
- this.callback = def.callback;
741
- }
742
- /**
743
- * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
744
- * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
745
- * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
746
- *
747
- * @param node A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
748
- * @returns The value of {@link module:ui/template~TemplateBinding#attribute} in
749
- * {@link module:ui/template~TemplateBinding#observable}.
750
- */
751
- getValue(node) {
752
- const value = this.observable[this.attribute];
753
- return this.callback ? this.callback(value, node) : value;
754
- }
755
- /**
756
- * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
757
- * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
758
- * value of {@link module:ui/template~TemplateValueSchema}.
759
- *
760
- * @param schema A full schema to generate an attribute or text in the DOM.
761
- * @param updater A DOM updater function used to update the native DOM attribute or text.
762
- * @param data Rendering data.
763
- * @returns A function to sever the listener binding.
764
- */
765
- activateAttributeListener(schema, updater, data) {
766
- const callback = () => syncValueSchemaValue(schema, updater, data);
767
- this.emitter.listenTo(this.observable, `change:${this.attribute}`, callback);
768
- // Allows revert of the listener.
769
- return () => {
770
- this.emitter.stopListening(this.observable, `change:${this.attribute}`, callback);
771
- };
772
- }
773
- }
774
- /**
775
- * Describes either:
776
- *
777
- * * a binding to an {@link module:utils/observablemixin~Observable},
778
- * * or a native DOM event binding.
779
- *
780
- * It is created by the {@link module:ui/template~BindChain#to} method.
781
- *
782
- * @internal
783
- */
784
- export class TemplateToBinding extends TemplateBinding {
785
- constructor(def) {
786
- super(def);
787
- this.eventNameOrFunction = def.eventNameOrFunction;
788
- }
789
- /**
790
- * Activates the listener for the native DOM event, which when fired, is propagated by
791
- * the {@link module:ui/template~TemplateBinding#emitter}.
792
- *
793
- * @param domEvtName The name of the native DOM event.
794
- * @param domSelector The selector in the DOM to filter delegated events.
795
- * @param data Rendering data.
796
- * @returns A function to sever the listener binding.
797
- */
798
- activateDomEventListener(domEvtName, domSelector, data) {
799
- const callback = (evt, domEvt) => {
800
- if (!domSelector || domEvt.target.matches(domSelector)) {
801
- if (typeof this.eventNameOrFunction == 'function') {
802
- this.eventNameOrFunction(domEvt);
803
- }
804
- else {
805
- this.observable.fire(this.eventNameOrFunction, domEvt);
806
- }
807
- }
808
- };
809
- this.emitter.listenTo(data.node, domEvtName, callback);
810
- // Allows revert of the listener.
811
- return () => {
812
- this.emitter.stopListening(data.node, domEvtName, callback);
813
- };
814
- }
815
- }
816
- /**
817
- * Describes a binding to {@link module:utils/observablemixin~Observable} created by the {@link module:ui/template~BindChain#if}
818
- * method.
819
- *
820
- * @internal
821
- */
822
- export class TemplateIfBinding extends TemplateBinding {
823
- constructor(def) {
824
- super(def);
825
- this.valueIfTrue = def.valueIfTrue;
826
- }
827
- /**
828
- * @inheritDoc
829
- */
830
- getValue(node) {
831
- const value = super.getValue(node);
832
- return isFalsy(value) ? false : (this.valueIfTrue || true);
833
- }
834
- }
835
- /**
836
- * Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
837
- * {@link module:ui/template~TemplateBinding}.
838
- */
839
- function hasTemplateBinding(schema) {
840
- if (!schema) {
841
- return false;
842
- }
843
- // Normalize attributes with additional data like namespace:
844
- //
845
- // class: {
846
- // ns: 'abc',
847
- // value: [ ... ]
848
- // }
849
- //
850
- if (schema.value) {
851
- schema = schema.value;
852
- }
853
- if (Array.isArray(schema)) {
854
- return schema.some(hasTemplateBinding);
855
- }
856
- else if (schema instanceof TemplateBinding) {
857
- return true;
858
- }
859
- return false;
860
- }
861
- /**
862
- * Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
863
- * an Array. Each entry of the Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
864
- * items.
865
- *
866
- * @param node DOM Node updated when {@link module:utils/observablemixin~Observable} changes.
867
- */
868
- function getValueSchemaValue(schema, node) {
869
- return schema.map(schemaItem => {
870
- // Process {@link module:ui/template~TemplateBinding} bindings.
871
- if (schemaItem instanceof TemplateBinding) {
872
- return schemaItem.getValue(node);
873
- }
874
- // All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
875
- return schemaItem;
876
- });
877
- }
878
- /**
879
- * A function executed each time the bound Observable attribute changes, which updates the DOM with a value
880
- * constructed from {@link module:ui/template~TemplateValueSchema}.
881
- *
882
- * @param updater A function which updates the DOM (like attribute or text).
883
- * @param node DOM Node updated when {@link module:utils/observablemixin~Observable} changes.
884
- */
885
- function syncValueSchemaValue(schema, updater, { node }) {
886
- const values = getValueSchemaValue(schema, node);
887
- let value;
888
- // Check if schema is a single Template.bind.if, like:
889
- //
890
- // class: Template.bind.if( 'foo' )
891
- //
892
- if (schema.length == 1 && schema[0] instanceof TemplateIfBinding) {
893
- value = values[0];
894
- }
895
- else {
896
- value = values.reduce(arrayValueReducer, '');
897
- }
898
- if (isFalsy(value)) {
899
- updater.remove();
900
- }
901
- else {
902
- updater.set(value);
903
- }
904
- }
905
- /**
906
- * Returns an object consisting of `set` and `remove` functions, which
907
- * can be used in the context of DOM Node to set or reset `textContent`.
908
- * @see module:ui/view~View#_bindToObservable
909
- *
910
- * @param node DOM Node to be modified.
911
- */
912
- function getTextUpdater(node) {
913
- return {
914
- set(value) {
915
- node.textContent = value;
916
- },
917
- remove() {
918
- node.textContent = '';
919
- }
920
- };
921
- }
922
- /**
923
- * Returns an object consisting of `set` and `remove` functions, which
924
- * can be used in the context of DOM Node to set or reset an attribute.
925
- * @see module:ui/view~View#_bindToObservable
926
- *
927
- * @param el DOM Node to be modified.
928
- * @param attrName Name of the attribute to be modified.
929
- * @param ns Namespace to use.
930
- */
931
- function getAttributeUpdater(el, attrName, ns) {
932
- return {
933
- set(value) {
934
- el.setAttributeNS(ns, attrName, value);
935
- },
936
- remove() {
937
- el.removeAttributeNS(ns, attrName);
938
- }
939
- };
940
- }
941
- /**
942
- * Returns an object consisting of `set` and `remove` functions, which
943
- * can be used in the context of CSSStyleDeclaration to set or remove a style.
944
- * @see module:ui/view~View#_bindToObservable
945
- *
946
- * @param el DOM Node to be modified.
947
- * @param styleName Name of the style to be modified.
948
- */
949
- function getStyleUpdater(el, styleName) {
950
- return {
951
- set(value) {
952
- el.style[styleName] = value;
953
- },
954
- remove() {
955
- el.style[styleName] = null;
956
- }
957
- };
958
- }
959
- /**
960
- * Clones definition of the template.
961
- */
962
- function clone(def) {
963
- const clone = cloneDeepWith(def, value => {
964
- // Don't clone the `Template.bind`* bindings because of the references to Observable
965
- // and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
966
- // cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
967
- // along with the definition.
968
- //
969
- // Don't clone Template instances if provided as a child. They're simply #render()ed
970
- // and nothing should interfere.
971
- //
972
- // Also don't clone View instances if provided as a child of the Template. The template
973
- // instance will be extracted from the View during the normalization and there's no need
974
- // to clone it.
975
- if (value && (value instanceof TemplateBinding || isTemplate(value) || isView(value) || isViewCollection(value))) {
976
- return value;
977
- }
978
- });
979
- return clone;
980
- }
981
- /**
982
- * Normalizes given {@link module:ui/template~TemplateDefinition}.
983
- *
984
- * See:
985
- * * {@link normalizeAttributes}
986
- * * {@link normalizeListeners}
987
- * * {@link normalizePlainTextDefinition}
988
- * * {@link normalizeTextDefinition}
989
- *
990
- * @param def A template definition.
991
- * @returns Normalized definition.
992
- */
993
- function normalize(def) {
994
- if (typeof def == 'string') {
995
- def = normalizePlainTextDefinition(def);
996
- }
997
- else if (def.text) {
998
- normalizeTextDefinition(def);
999
- }
1000
- if (def.on) {
1001
- def.eventListeners = normalizeListeners(def.on);
1002
- // Template mixes EmitterMixin, so delete #on to avoid collision.
1003
- delete def.on;
1004
- }
1005
- if (!def.text) {
1006
- if (def.attributes) {
1007
- normalizeAttributes(def.attributes);
1008
- }
1009
- const children = [];
1010
- if (def.children) {
1011
- if (isViewCollection(def.children)) {
1012
- children.push(def.children);
1013
- }
1014
- else {
1015
- for (const child of def.children) {
1016
- if (isTemplate(child) || isView(child) || isNode(child)) {
1017
- children.push(child);
1018
- }
1019
- else {
1020
- children.push(new Template(child));
1021
- }
1022
- }
1023
- }
1024
- }
1025
- def.children = children;
1026
- }
1027
- return def;
1028
- }
1029
- /**
1030
- * Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
1031
- *
1032
- * ```
1033
- * attributes: {
1034
- * a: 'bar',
1035
- * b: {@link module:ui/template~TemplateBinding},
1036
- * c: {
1037
- * value: 'bar'
1038
- * }
1039
- * }
1040
- * ```
1041
- *
1042
- * becomes
1043
- *
1044
- * ```
1045
- * attributes: {
1046
- * a: [ 'bar' ],
1047
- * b: [ {@link module:ui/template~TemplateBinding} ],
1048
- * c: {
1049
- * value: [ 'bar' ]
1050
- * }
1051
- * }
1052
- * ```
1053
- */
1054
- function normalizeAttributes(attributes) {
1055
- for (const a in attributes) {
1056
- if (attributes[a].value) {
1057
- attributes[a].value = toArray(attributes[a].value);
1058
- }
1059
- arrayify(attributes, a);
1060
- }
1061
- }
1062
- /**
1063
- * Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
1064
- *
1065
- * ```
1066
- * on: {
1067
- * a: 'bar',
1068
- * b: {@link module:ui/template~TemplateBinding},
1069
- * c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
1070
- * }
1071
- * ```
1072
- *
1073
- * becomes
1074
- *
1075
- * ```
1076
- * on: {
1077
- * a: [ 'bar' ],
1078
- * b: [ {@link module:ui/template~TemplateBinding} ],
1079
- * c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
1080
- * }
1081
- * ```
1082
- *
1083
- * @returns Object containing normalized listeners.
1084
- */
1085
- function normalizeListeners(listeners) {
1086
- for (const l in listeners) {
1087
- arrayify(listeners, l);
1088
- }
1089
- return listeners;
1090
- }
1091
- /**
1092
- * Normalizes "string" {@link module:ui/template~TemplateDefinition}.
1093
- *
1094
- * ```
1095
- * "foo"
1096
- * ```
1097
- *
1098
- * becomes
1099
- *
1100
- * ```
1101
- * { text: [ 'foo' ] },
1102
- * ```
1103
- *
1104
- * @returns Normalized template definition.
1105
- */
1106
- function normalizePlainTextDefinition(def) {
1107
- return {
1108
- text: [def]
1109
- };
1110
- }
1111
- /**
1112
- * Normalizes text {@link module:ui/template~TemplateDefinition}.
1113
- *
1114
- * ```
1115
- * children: [
1116
- * { text: 'def' },
1117
- * { text: {@link module:ui/template~TemplateBinding} }
1118
- * ]
1119
- * ```
1120
- *
1121
- * becomes
1122
- *
1123
- * ```
1124
- * children: [
1125
- * { text: [ 'def' ] },
1126
- * { text: [ {@link module:ui/template~TemplateBinding} ] }
1127
- * ]
1128
- * ```
1129
- */
1130
- function normalizeTextDefinition(def) {
1131
- def.text = toArray(def.text);
1132
- }
1133
- /**
1134
- * Wraps an entry in Object in an Array, if not already one.
1135
- *
1136
- * ```
1137
- * {
1138
- * x: 'y',
1139
- * a: [ 'b' ]
1140
- * }
1141
- * ```
1142
- *
1143
- * becomes
1144
- *
1145
- * ```
1146
- * {
1147
- * x: [ 'y' ],
1148
- * a: [ 'b' ]
1149
- * }
1150
- * ```
1151
- */
1152
- function arrayify(obj, key) {
1153
- obj[key] = toArray(obj[key]);
1154
- }
1155
- /**
1156
- * A helper which concatenates the value avoiding unwanted
1157
- * leading white spaces.
1158
- */
1159
- function arrayValueReducer(prev, cur) {
1160
- if (isFalsy(cur)) {
1161
- return prev;
1162
- }
1163
- else if (isFalsy(prev)) {
1164
- return cur;
1165
- }
1166
- else {
1167
- return `${prev} ${cur}`;
1168
- }
1169
- }
1170
- /**
1171
- * Extends one object defined in the following format:
1172
- *
1173
- * ```
1174
- * {
1175
- * key1: [Array1],
1176
- * key2: [Array2],
1177
- * ...
1178
- * keyN: [ArrayN]
1179
- * }
1180
- * ```
1181
- *
1182
- * with another object of the same data format.
1183
- *
1184
- * @param obj Base object.
1185
- * @param ext Object extending base.
1186
- */
1187
- function extendObjectValueArray(obj, ext) {
1188
- for (const a in ext) {
1189
- if (obj[a]) {
1190
- obj[a].push(...ext[a]);
1191
- }
1192
- else {
1193
- obj[a] = ext[a];
1194
- }
1195
- }
1196
- }
1197
- /**
1198
- * A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
1199
- * with content from {@link module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
1200
- *
1201
- * @param def A template instance to be extended.
1202
- * @param def A definition which is to extend the template instance.
1203
- * @param Error context.
1204
- */
1205
- function extendTemplate(template, def) {
1206
- if (def.attributes) {
1207
- if (!template.attributes) {
1208
- template.attributes = {};
1209
- }
1210
- extendObjectValueArray(template.attributes, def.attributes);
1211
- }
1212
- if (def.eventListeners) {
1213
- if (!template.eventListeners) {
1214
- template.eventListeners = {};
1215
- }
1216
- extendObjectValueArray(template.eventListeners, def.eventListeners);
1217
- }
1218
- if (def.text) {
1219
- template.text.push(...def.text);
1220
- }
1221
- if (def.children && def.children.length) {
1222
- if (template.children.length != def.children.length) {
1223
- /**
1224
- * The number of children in extended definition does not match.
1225
- *
1226
- * @error ui-template-extend-children-mismatch
1227
- */
1228
- throw new CKEditorError('ui-template-extend-children-mismatch', template);
1229
- }
1230
- let childIndex = 0;
1231
- for (const childDef of def.children) {
1232
- extendTemplate(template.children[childIndex++], childDef);
1233
- }
1234
- }
1235
- }
1236
- /**
1237
- * Checks if value is "falsy".
1238
- * Note: 0 (Number) is not "falsy" in this context.
1239
- *
1240
- * @param value Value to be checked.
1241
- */
1242
- function isFalsy(value) {
1243
- return !value && value !== 0;
1244
- }
1245
- /**
1246
- * Checks if the item is an instance of {@link module:ui/view~View}
1247
- *
1248
- * @param value Value to be checked.
1249
- */
1250
- function isView(item) {
1251
- return item instanceof View;
1252
- }
1253
- /**
1254
- * Checks if the item is an instance of {@link module:ui/template~Template}
1255
- *
1256
- * @param value Value to be checked.
1257
- */
1258
- function isTemplate(item) {
1259
- return item instanceof Template;
1260
- }
1261
- /**
1262
- * Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
1263
- *
1264
- * @param value Value to be checked.
1265
- */
1266
- function isViewCollection(item) {
1267
- return item instanceof ViewCollection;
1268
- }
1269
- /**
1270
- * Checks if value array contains the one with namespace.
1271
- */
1272
- function isNamespaced(attrValue) {
1273
- return isObject(attrValue[0]) && attrValue[0].ns;
1274
- }
1275
- /**
1276
- * Creates an empty skeleton for {@link module:ui/template~Template#revert}
1277
- * data.
1278
- */
1279
- function getEmptyRevertData() {
1280
- return {
1281
- children: [],
1282
- bindings: [],
1283
- attributes: {}
1284
- };
1285
- }
1286
- /**
1287
- * Checks whether an attribute should be extended when
1288
- * {@link module:ui/template~Template#apply} is called.
1289
- *
1290
- * @param attrName Attribute name to check.
1291
- */
1292
- function shouldExtend(attrName) {
1293
- return attrName == 'class' || attrName == 'style';
1294
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/template
7
+ */
8
+ /* global document */
9
+ import View from './view';
10
+ import ViewCollection from './viewcollection';
11
+ import { CKEditorError, EmitterMixin, isNode, toArray } from '@ckeditor/ckeditor5-utils';
12
+ import { isObject, cloneDeepWith } from 'lodash-es';
13
+ const xhtmlNs = 'http://www.w3.org/1999/xhtml';
14
+ /**
15
+ * A basic Template class. It renders a DOM HTML element or text from a
16
+ * {@link module:ui/template~TemplateDefinition definition} and supports element attributes, children,
17
+ * bindings to {@link module:utils/observablemixin~Observable observables} and DOM event propagation.
18
+ *
19
+ * A simple template can look like this:
20
+ *
21
+ * ```ts
22
+ * const bind = Template.bind( observable, emitter );
23
+ *
24
+ * new Template( {
25
+ * tag: 'p',
26
+ * attributes: {
27
+ * class: 'foo',
28
+ * style: {
29
+ * backgroundColor: 'yellow'
30
+ * }
31
+ * },
32
+ * on: {
33
+ * click: bind.to( 'clicked' )
34
+ * },
35
+ * children: [
36
+ * 'A paragraph.'
37
+ * ]
38
+ * } ).render();
39
+ * ```
40
+ *
41
+ * and it will render the following HTML element:
42
+ *
43
+ * ```html
44
+ * <p class="foo" style="background-color: yellow;">A paragraph.</p>
45
+ * ```
46
+ *
47
+ * Additionally, the `observable` will always fire `clicked` upon clicking `<p>` in the DOM.
48
+ *
49
+ * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex
50
+ * template definitions.
51
+ */
52
+ export default class Template extends EmitterMixin() {
53
+ /**
54
+ * Creates an instance of the {@link ~Template} class.
55
+ *
56
+ * @param def The definition of the template.
57
+ */
58
+ constructor(def) {
59
+ super();
60
+ Object.assign(this, normalize(clone(def)));
61
+ this._isRendered = false;
62
+ this._revertData = null;
63
+ }
64
+ /**
65
+ * Renders a DOM Node (an HTML element or text) out of the template.
66
+ *
67
+ * ```ts
68
+ * const domNode = new Template( { ... } ).render();
69
+ * ```
70
+ *
71
+ * See: {@link #apply}.
72
+ */
73
+ render() {
74
+ const node = this._renderNode({
75
+ intoFragment: true
76
+ });
77
+ this._isRendered = true;
78
+ return node;
79
+ }
80
+ /**
81
+ * Applies the template to an existing DOM Node, either HTML element or text.
82
+ *
83
+ * **Note:** No new DOM nodes will be created. Applying extends:
84
+ *
85
+ * {@link module:ui/template~TemplateDefinition attributes},
86
+ * {@link module:ui/template~TemplateDefinition event listeners}, and
87
+ * `textContent` of {@link module:ui/template~TemplateDefinition children} only.
88
+ *
89
+ * **Note:** Existing `class` and `style` attributes are extended when a template
90
+ * is applied to an HTML element, while other attributes and `textContent` are overridden.
91
+ *
92
+ * **Note:** The process of applying a template can be easily reverted using the
93
+ * {@link module:ui/template~Template#revert} method.
94
+ *
95
+ * ```ts
96
+ * const element = document.createElement( 'div' );
97
+ * const observable = new Model( { divClass: 'my-div' } );
98
+ * const emitter = Object.create( EmitterMixin );
99
+ * const bind = Template.bind( observable, emitter );
100
+ *
101
+ * new Template( {
102
+ * attributes: {
103
+ * id: 'first-div',
104
+ * class: bind.to( 'divClass' )
105
+ * },
106
+ * on: {
107
+ * click: bind( 'elementClicked' ) // Will be fired by the observable.
108
+ * },
109
+ * children: [
110
+ * 'Div text.'
111
+ * ]
112
+ * } ).apply( element );
113
+ *
114
+ * console.log( element.outerHTML ); // -> '<div id="first-div" class="my-div"></div>'
115
+ * ```
116
+ *
117
+ * @see module:ui/template~Template#render
118
+ * @see module:ui/template~Template#revert
119
+ * @param node Root node for the template to apply.
120
+ */
121
+ apply(node) {
122
+ this._revertData = getEmptyRevertData();
123
+ this._renderNode({
124
+ node,
125
+ intoFragment: false,
126
+ isApplying: true,
127
+ revertData: this._revertData
128
+ });
129
+ return node;
130
+ }
131
+ /**
132
+ * Reverts a template {@link module:ui/template~Template#apply applied} to a DOM node.
133
+ *
134
+ * @param node The root node for the template to revert. In most of the cases, it is the
135
+ * same node used by {@link module:ui/template~Template#apply}.
136
+ */
137
+ revert(node) {
138
+ if (!this._revertData) {
139
+ /**
140
+ * Attempting to revert a template which has not been applied yet.
141
+ *
142
+ * @error ui-template-revert-not-applied
143
+ */
144
+ throw new CKEditorError('ui-template-revert-not-applied', [this, node]);
145
+ }
146
+ this._revertTemplateFromNode(node, this._revertData);
147
+ }
148
+ /**
149
+ * Returns an iterator which traverses the template in search of {@link module:ui/view~View}
150
+ * instances and returns them one by one.
151
+ *
152
+ * ```ts
153
+ * const viewFoo = new View();
154
+ * const viewBar = new View();
155
+ * const viewBaz = new View();
156
+ * const template = new Template( {
157
+ * tag: 'div',
158
+ * children: [
159
+ * viewFoo,
160
+ * {
161
+ * tag: 'div',
162
+ * children: [
163
+ * viewBar
164
+ * ]
165
+ * },
166
+ * viewBaz
167
+ * ]
168
+ * } );
169
+ *
170
+ * // Logs: viewFoo, viewBar, viewBaz
171
+ * for ( const view of template.getViews() ) {
172
+ * console.log( view );
173
+ * }
174
+ * ```
175
+ */
176
+ *getViews() {
177
+ function* search(def) {
178
+ if (def.children) {
179
+ for (const child of def.children) {
180
+ if (isView(child)) {
181
+ yield child;
182
+ }
183
+ else if (isTemplate(child)) {
184
+ yield* search(child);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ yield* search(this);
190
+ }
191
+ /**
192
+ * An entry point to the interface which binds DOM nodes to
193
+ * {@link module:utils/observablemixin~Observable observables}.
194
+ * There are two types of bindings:
195
+ *
196
+ * * HTML element attributes or text `textContent` synchronized with attributes of an
197
+ * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}
198
+ * and {@link module:ui/template~BindChain#if}.
199
+ *
200
+ * ```ts
201
+ * const bind = Template.bind( observable, emitter );
202
+ *
203
+ * new Template( {
204
+ * attributes: {
205
+ * // Binds the element "class" attribute to observable#classAttribute.
206
+ * class: bind.to( 'classAttribute' )
207
+ * }
208
+ * } ).render();
209
+ * ```
210
+ *
211
+ * * DOM events fired on HTML element propagated through
212
+ * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}.
213
+ *
214
+ * ```ts
215
+ * const bind = Template.bind( observable, emitter );
216
+ *
217
+ * new Template( {
218
+ * on: {
219
+ * // Will be fired by the observable.
220
+ * click: bind( 'elementClicked' )
221
+ * }
222
+ * } ).render();
223
+ * ```
224
+ *
225
+ * Also see {@link module:ui/view~View#bindTemplate}.
226
+ *
227
+ * @param observable An observable which provides boundable attributes.
228
+ * @param emitter An emitter that listens to observable attribute
229
+ * changes or DOM Events (depending on the kind of the binding). Usually, a {@link module:ui/view~View} instance.
230
+ */
231
+ static bind(observable, emitter) {
232
+ return {
233
+ to(eventNameOrFunctionOrAttribute, callback) {
234
+ return new TemplateToBinding({
235
+ eventNameOrFunction: eventNameOrFunctionOrAttribute,
236
+ attribute: eventNameOrFunctionOrAttribute,
237
+ observable, emitter, callback
238
+ });
239
+ },
240
+ if(attribute, valueIfTrue, callback) {
241
+ return new TemplateIfBinding({
242
+ observable, emitter, attribute, valueIfTrue, callback
243
+ });
244
+ }
245
+ };
246
+ }
247
+ /**
248
+ * Extends an existing {@link module:ui/template~Template} instance with some additional content
249
+ * from another {@link module:ui/template~TemplateDefinition}.
250
+ *
251
+ * ```ts
252
+ * const bind = Template.bind( observable, emitter );
253
+ *
254
+ * const template = new Template( {
255
+ * tag: 'p',
256
+ * attributes: {
257
+ * class: 'a',
258
+ * data-x: bind.to( 'foo' )
259
+ * },
260
+ * children: [
261
+ * {
262
+ * tag: 'span',
263
+ * attributes: {
264
+ * class: 'b'
265
+ * },
266
+ * children: [
267
+ * 'Span'
268
+ * ]
269
+ * }
270
+ * ]
271
+ * } );
272
+ *
273
+ * // Instance-level extension.
274
+ * Template.extend( template, {
275
+ * attributes: {
276
+ * class: 'b',
277
+ * data-x: bind.to( 'bar' )
278
+ * },
279
+ * children: [
280
+ * {
281
+ * attributes: {
282
+ * class: 'c'
283
+ * }
284
+ * }
285
+ * ]
286
+ * } );
287
+ *
288
+ * // Child extension.
289
+ * Template.extend( template.children[ 0 ], {
290
+ * attributes: {
291
+ * class: 'd'
292
+ * }
293
+ * } );
294
+ * ```
295
+ *
296
+ * the `outerHTML` of `template.render()` is:
297
+ *
298
+ * ```html
299
+ * <p class="a b" data-x="{ observable.foo } { observable.bar }">
300
+ * <span class="b c d">Span</span>
301
+ * </p>
302
+ * ```
303
+ *
304
+ * @param template An existing template instance to be extended.
305
+ * @param def Additional definition to be applied to a template.
306
+ */
307
+ static extend(template, def) {
308
+ if (template._isRendered) {
309
+ /**
310
+ * Extending a template after rendering may not work as expected. To make sure
311
+ * the {@link module:ui/template~Template.extend extending} works for an element,
312
+ * make sure it happens before {@link module:ui/template~Template#render} is called.
313
+ *
314
+ * @error template-extend-render
315
+ */
316
+ throw new CKEditorError('template-extend-render', [this, template]);
317
+ }
318
+ extendTemplate(template, normalize(clone(def)));
319
+ }
320
+ /**
321
+ * Renders a DOM Node (either an HTML element or text) out of the template.
322
+ *
323
+ * @param data Rendering data.
324
+ */
325
+ _renderNode(data) {
326
+ let isInvalid;
327
+ if (data.node) {
328
+ // When applying, a definition cannot have "tag" and "text" at the same time.
329
+ isInvalid = this.tag && this.text;
330
+ }
331
+ else {
332
+ // When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
333
+ isInvalid = this.tag ? this.text : !this.text;
334
+ }
335
+ if (isInvalid) {
336
+ /**
337
+ * Node definition cannot have the "tag" and "text" properties at the same time.
338
+ * Node definition must have either "tag" or "text" when rendering a new Node.
339
+ *
340
+ * @error ui-template-wrong-syntax
341
+ */
342
+ throw new CKEditorError('ui-template-wrong-syntax', this);
343
+ }
344
+ if (this.text) {
345
+ return this._renderText(data);
346
+ }
347
+ else {
348
+ return this._renderElement(data);
349
+ }
350
+ }
351
+ /**
352
+ * Renders an HTML element out of the template.
353
+ *
354
+ * @param data Rendering data.
355
+ */
356
+ _renderElement(data) {
357
+ let node = data.node;
358
+ if (!node) {
359
+ node = data.node = document.createElementNS(this.ns || xhtmlNs, this.tag);
360
+ }
361
+ this._renderAttributes(data);
362
+ this._renderElementChildren(data);
363
+ this._setUpListeners(data);
364
+ return node;
365
+ }
366
+ /**
367
+ * Renders a text node out of {@link module:ui/template~Template#text}.
368
+ *
369
+ * @param data Rendering data.
370
+ */
371
+ _renderText(data) {
372
+ let node = data.node;
373
+ // Save the original textContent to revert it in #revert().
374
+ if (node) {
375
+ data.revertData.text = node.textContent;
376
+ }
377
+ else {
378
+ node = data.node = document.createTextNode('');
379
+ }
380
+ // Check if this Text Node is bound to Observable. Cases:
381
+ //
382
+ // text: [ Template.bind( ... ).to( ... ) ]
383
+ //
384
+ // text: [
385
+ // 'foo',
386
+ // Template.bind( ... ).to( ... ),
387
+ // ...
388
+ // ]
389
+ //
390
+ if (hasTemplateBinding(this.text)) {
391
+ this._bindToObservable({
392
+ schema: this.text,
393
+ updater: getTextUpdater(node),
394
+ data
395
+ });
396
+ }
397
+ // Simply set text. Cases:
398
+ //
399
+ // text: [ 'all', 'are', 'static' ]
400
+ //
401
+ // text: [ 'foo' ]
402
+ //
403
+ else {
404
+ node.textContent = this.text.join('');
405
+ }
406
+ return node;
407
+ }
408
+ /**
409
+ * Renders HTML element attributes out of {@link module:ui/template~Template#attributes}.
410
+ *
411
+ * @param data Rendering data.
412
+ */
413
+ _renderAttributes(data) {
414
+ if (!this.attributes) {
415
+ return;
416
+ }
417
+ const node = data.node;
418
+ const revertData = data.revertData;
419
+ for (const attrName in this.attributes) {
420
+ // Current attribute value in DOM.
421
+ const domAttrValue = node.getAttribute(attrName);
422
+ // The value to be set.
423
+ const attrValue = this.attributes[attrName];
424
+ // Save revert data.
425
+ if (revertData) {
426
+ revertData.attributes[attrName] = domAttrValue;
427
+ }
428
+ // Detect custom namespace:
429
+ //
430
+ // class: {
431
+ // ns: 'abc',
432
+ // value: Template.bind( ... ).to( ... )
433
+ // }
434
+ //
435
+ const attrNs = isNamespaced(attrValue) ? attrValue[0].ns : null;
436
+ // Activate binding if one is found. Cases:
437
+ //
438
+ // class: [
439
+ // Template.bind( ... ).to( ... )
440
+ // ]
441
+ //
442
+ // class: [
443
+ // 'bar',
444
+ // Template.bind( ... ).to( ... ),
445
+ // 'baz'
446
+ // ]
447
+ //
448
+ // class: {
449
+ // ns: 'abc',
450
+ // value: Template.bind( ... ).to( ... )
451
+ // }
452
+ //
453
+ if (hasTemplateBinding(attrValue)) {
454
+ // Normalize attributes with additional data like namespace:
455
+ //
456
+ // class: {
457
+ // ns: 'abc',
458
+ // value: [ ... ]
459
+ // }
460
+ //
461
+ const valueToBind = isNamespaced(attrValue) ? attrValue[0].value : attrValue;
462
+ // Extend the original value of attributes like "style" and "class",
463
+ // don't override them.
464
+ if (revertData && shouldExtend(attrName)) {
465
+ valueToBind.unshift(domAttrValue);
466
+ }
467
+ this._bindToObservable({
468
+ schema: valueToBind,
469
+ updater: getAttributeUpdater(node, attrName, attrNs),
470
+ data
471
+ });
472
+ }
473
+ // Style attribute could be an Object so it needs to be parsed in a specific way.
474
+ //
475
+ // style: {
476
+ // width: '100px',
477
+ // height: Template.bind( ... ).to( ... )
478
+ // }
479
+ //
480
+ else if (attrName == 'style' && typeof attrValue[0] !== 'string') {
481
+ this._renderStyleAttribute(attrValue[0], data);
482
+ }
483
+ // Otherwise simply set the static attribute:
484
+ //
485
+ // class: [ 'foo' ]
486
+ //
487
+ // class: [ 'all', 'are', 'static' ]
488
+ //
489
+ // class: [
490
+ // {
491
+ // ns: 'abc',
492
+ // value: [ 'foo' ]
493
+ // }
494
+ // ]
495
+ //
496
+ else {
497
+ // Extend the original value of attributes like "style" and "class",
498
+ // don't override them.
499
+ if (revertData && domAttrValue && shouldExtend(attrName)) {
500
+ attrValue.unshift(domAttrValue);
501
+ }
502
+ const value = attrValue
503
+ // Retrieve "values" from:
504
+ //
505
+ // class: [
506
+ // {
507
+ // ns: 'abc',
508
+ // value: [ ... ]
509
+ // }
510
+ // ]
511
+ //
512
+ .map((val) => val ? (val.value || val) : val)
513
+ // Flatten the array.
514
+ .reduce((prev, next) => prev.concat(next), [])
515
+ // Convert into string.
516
+ .reduce(arrayValueReducer, '');
517
+ if (!isFalsy(value)) {
518
+ node.setAttributeNS(attrNs, attrName, value);
519
+ }
520
+ }
521
+ }
522
+ }
523
+ /**
524
+ * Renders the `style` attribute of an HTML element based on
525
+ * {@link module:ui/template~Template#attributes}.
526
+ *
527
+ * A style attribute is an object with static values:
528
+ *
529
+ * ```ts
530
+ * attributes: {
531
+ * style: {
532
+ * color: 'red'
533
+ * }
534
+ * }
535
+ * ```
536
+ *
537
+ * or values bound to {@link module:ui/model~Model} properties:
538
+ *
539
+ * ```ts
540
+ * attributes: {
541
+ * style: {
542
+ * color: bind.to( ... )
543
+ * }
544
+ * }
545
+ * ```
546
+ *
547
+ * Note: The `style` attribute is rendered without setting the namespace. It does not seem to be
548
+ * needed.
549
+ *
550
+ * @param styles Styles located in `attributes.style` of {@link module:ui/template~TemplateDefinition}.
551
+ * @param data Rendering data.
552
+ */
553
+ _renderStyleAttribute(styles, data) {
554
+ const node = data.node;
555
+ for (const styleName in styles) {
556
+ const styleValue = styles[styleName];
557
+ // Cases:
558
+ //
559
+ // style: {
560
+ // color: bind.to( 'attribute' )
561
+ // }
562
+ //
563
+ if (hasTemplateBinding(styleValue)) {
564
+ this._bindToObservable({
565
+ schema: [styleValue],
566
+ updater: getStyleUpdater(node, styleName),
567
+ data
568
+ });
569
+ }
570
+ // Cases:
571
+ //
572
+ // style: {
573
+ // color: 'red'
574
+ // }
575
+ //
576
+ else {
577
+ node.style[styleName] = styleValue;
578
+ }
579
+ }
580
+ }
581
+ /**
582
+ * Recursively renders HTML element's children from {@link module:ui/template~Template#children}.
583
+ *
584
+ * @param data Rendering data.
585
+ */
586
+ _renderElementChildren(data) {
587
+ const node = data.node;
588
+ const container = data.intoFragment ? document.createDocumentFragment() : node;
589
+ const isApplying = data.isApplying;
590
+ let childIndex = 0;
591
+ for (const child of this.children) {
592
+ if (isViewCollection(child)) {
593
+ if (!isApplying) {
594
+ child.setParent(node);
595
+ // Note: ViewCollection renders its children.
596
+ for (const view of child) {
597
+ container.appendChild(view.element);
598
+ }
599
+ }
600
+ }
601
+ else if (isView(child)) {
602
+ if (!isApplying) {
603
+ if (!child.isRendered) {
604
+ child.render();
605
+ }
606
+ container.appendChild(child.element);
607
+ }
608
+ }
609
+ else if (isNode(child)) {
610
+ container.appendChild(child);
611
+ }
612
+ else {
613
+ if (isApplying) {
614
+ const revertData = data.revertData;
615
+ const childRevertData = getEmptyRevertData();
616
+ revertData.children.push(childRevertData);
617
+ child._renderNode({
618
+ intoFragment: false,
619
+ node: container.childNodes[childIndex++],
620
+ isApplying: true,
621
+ revertData: childRevertData
622
+ });
623
+ }
624
+ else {
625
+ container.appendChild(child.render());
626
+ }
627
+ }
628
+ }
629
+ if (data.intoFragment) {
630
+ node.appendChild(container);
631
+ }
632
+ }
633
+ /**
634
+ * Activates `on` event listeners from the {@link module:ui/template~TemplateDefinition}
635
+ * on an HTML element.
636
+ *
637
+ * @param data Rendering data.
638
+ */
639
+ _setUpListeners(data) {
640
+ if (!this.eventListeners) {
641
+ return;
642
+ }
643
+ for (const key in this.eventListeners) {
644
+ const revertBindings = this.eventListeners[key].map(schemaItem => {
645
+ const [domEvtName, domSelector] = key.split('@');
646
+ return schemaItem.activateDomEventListener(domEvtName, domSelector, data);
647
+ });
648
+ if (data.revertData) {
649
+ data.revertData.bindings.push(revertBindings);
650
+ }
651
+ }
652
+ }
653
+ /**
654
+ * For a given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding}
655
+ * activates the binding and sets its initial value.
656
+ *
657
+ * Note: {@link module:ui/template~TemplateValueSchema} can be for HTML element attributes or
658
+ * text node `textContent`.
659
+ *
660
+ * @param options Binding options.
661
+ * @param options.updater A function which updates the DOM (like attribute or text).
662
+ * @param options.data Rendering data.
663
+ */
664
+ _bindToObservable({ schema, updater, data }) {
665
+ const revertData = data.revertData;
666
+ // Set initial values.
667
+ syncValueSchemaValue(schema, updater, data);
668
+ const revertBindings = schema
669
+ // Filter "falsy" (false, undefined, null, '') value schema components out.
670
+ .filter(item => !isFalsy(item))
671
+ // Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
672
+ .filter((item) => item.observable)
673
+ // Once only the actual binding are left, let the emitter listen to observable change:attribute event.
674
+ // TODO: Reduce the number of listeners attached as many bindings may listen
675
+ // to the same observable attribute.
676
+ .map(templateBinding => templateBinding.activateAttributeListener(schema, updater, data));
677
+ if (revertData) {
678
+ revertData.bindings.push(revertBindings);
679
+ }
680
+ }
681
+ /**
682
+ * Reverts {@link module:ui/template~RenderData#revertData template data} from a node to
683
+ * return it to the original state.
684
+ *
685
+ * @param node A node to be reverted.
686
+ * @param revertData An object that stores information about what changes have been made by
687
+ * {@link #apply} to the node. See {@link module:ui/template~RenderData#revertData} for more information.
688
+ */
689
+ _revertTemplateFromNode(node, revertData) {
690
+ for (const binding of revertData.bindings) {
691
+ // Each binding may consist of several observable+observable#attribute.
692
+ // like the following has 2:
693
+ //
694
+ // class: [
695
+ // 'x',
696
+ // bind.to( 'foo' ),
697
+ // 'y',
698
+ // bind.to( 'bar' )
699
+ // ]
700
+ //
701
+ for (const revertBinding of binding) {
702
+ revertBinding();
703
+ }
704
+ }
705
+ if (revertData.text) {
706
+ node.textContent = revertData.text;
707
+ return;
708
+ }
709
+ const element = node;
710
+ for (const attrName in revertData.attributes) {
711
+ const attrValue = revertData.attributes[attrName];
712
+ // When the attribute has **not** been set before #apply().
713
+ if (attrValue === null) {
714
+ element.removeAttribute(attrName);
715
+ }
716
+ else {
717
+ element.setAttribute(attrName, attrValue);
718
+ }
719
+ }
720
+ for (let i = 0; i < revertData.children.length; ++i) {
721
+ this._revertTemplateFromNode(element.childNodes[i], revertData.children[i]);
722
+ }
723
+ }
724
+ }
725
+ /**
726
+ * Describes a binding created by the {@link module:ui/template~Template.bind} interface.
727
+ *
728
+ * @internal
729
+ */
730
+ export class TemplateBinding {
731
+ /**
732
+ * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
733
+ *
734
+ * @param def The definition of the binding.
735
+ */
736
+ constructor(def) {
737
+ this.attribute = def.attribute;
738
+ this.observable = def.observable;
739
+ this.emitter = def.emitter;
740
+ this.callback = def.callback;
741
+ }
742
+ /**
743
+ * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
744
+ * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
745
+ * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
746
+ *
747
+ * @param node A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
748
+ * @returns The value of {@link module:ui/template~TemplateBinding#attribute} in
749
+ * {@link module:ui/template~TemplateBinding#observable}.
750
+ */
751
+ getValue(node) {
752
+ const value = this.observable[this.attribute];
753
+ return this.callback ? this.callback(value, node) : value;
754
+ }
755
+ /**
756
+ * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
757
+ * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
758
+ * value of {@link module:ui/template~TemplateValueSchema}.
759
+ *
760
+ * @param schema A full schema to generate an attribute or text in the DOM.
761
+ * @param updater A DOM updater function used to update the native DOM attribute or text.
762
+ * @param data Rendering data.
763
+ * @returns A function to sever the listener binding.
764
+ */
765
+ activateAttributeListener(schema, updater, data) {
766
+ const callback = () => syncValueSchemaValue(schema, updater, data);
767
+ this.emitter.listenTo(this.observable, `change:${this.attribute}`, callback);
768
+ // Allows revert of the listener.
769
+ return () => {
770
+ this.emitter.stopListening(this.observable, `change:${this.attribute}`, callback);
771
+ };
772
+ }
773
+ }
774
+ /**
775
+ * Describes either:
776
+ *
777
+ * * a binding to an {@link module:utils/observablemixin~Observable},
778
+ * * or a native DOM event binding.
779
+ *
780
+ * It is created by the {@link module:ui/template~BindChain#to} method.
781
+ *
782
+ * @internal
783
+ */
784
+ export class TemplateToBinding extends TemplateBinding {
785
+ constructor(def) {
786
+ super(def);
787
+ this.eventNameOrFunction = def.eventNameOrFunction;
788
+ }
789
+ /**
790
+ * Activates the listener for the native DOM event, which when fired, is propagated by
791
+ * the {@link module:ui/template~TemplateBinding#emitter}.
792
+ *
793
+ * @param domEvtName The name of the native DOM event.
794
+ * @param domSelector The selector in the DOM to filter delegated events.
795
+ * @param data Rendering data.
796
+ * @returns A function to sever the listener binding.
797
+ */
798
+ activateDomEventListener(domEvtName, domSelector, data) {
799
+ const callback = (evt, domEvt) => {
800
+ if (!domSelector || domEvt.target.matches(domSelector)) {
801
+ if (typeof this.eventNameOrFunction == 'function') {
802
+ this.eventNameOrFunction(domEvt);
803
+ }
804
+ else {
805
+ this.observable.fire(this.eventNameOrFunction, domEvt);
806
+ }
807
+ }
808
+ };
809
+ this.emitter.listenTo(data.node, domEvtName, callback);
810
+ // Allows revert of the listener.
811
+ return () => {
812
+ this.emitter.stopListening(data.node, domEvtName, callback);
813
+ };
814
+ }
815
+ }
816
+ /**
817
+ * Describes a binding to {@link module:utils/observablemixin~Observable} created by the {@link module:ui/template~BindChain#if}
818
+ * method.
819
+ *
820
+ * @internal
821
+ */
822
+ export class TemplateIfBinding extends TemplateBinding {
823
+ constructor(def) {
824
+ super(def);
825
+ this.valueIfTrue = def.valueIfTrue;
826
+ }
827
+ /**
828
+ * @inheritDoc
829
+ */
830
+ getValue(node) {
831
+ const value = super.getValue(node);
832
+ return isFalsy(value) ? false : (this.valueIfTrue || true);
833
+ }
834
+ }
835
+ /**
836
+ * Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
837
+ * {@link module:ui/template~TemplateBinding}.
838
+ */
839
+ function hasTemplateBinding(schema) {
840
+ if (!schema) {
841
+ return false;
842
+ }
843
+ // Normalize attributes with additional data like namespace:
844
+ //
845
+ // class: {
846
+ // ns: 'abc',
847
+ // value: [ ... ]
848
+ // }
849
+ //
850
+ if (schema.value) {
851
+ schema = schema.value;
852
+ }
853
+ if (Array.isArray(schema)) {
854
+ return schema.some(hasTemplateBinding);
855
+ }
856
+ else if (schema instanceof TemplateBinding) {
857
+ return true;
858
+ }
859
+ return false;
860
+ }
861
+ /**
862
+ * Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
863
+ * an Array. Each entry of the Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
864
+ * items.
865
+ *
866
+ * @param node DOM Node updated when {@link module:utils/observablemixin~Observable} changes.
867
+ */
868
+ function getValueSchemaValue(schema, node) {
869
+ return schema.map(schemaItem => {
870
+ // Process {@link module:ui/template~TemplateBinding} bindings.
871
+ if (schemaItem instanceof TemplateBinding) {
872
+ return schemaItem.getValue(node);
873
+ }
874
+ // All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
875
+ return schemaItem;
876
+ });
877
+ }
878
+ /**
879
+ * A function executed each time the bound Observable attribute changes, which updates the DOM with a value
880
+ * constructed from {@link module:ui/template~TemplateValueSchema}.
881
+ *
882
+ * @param updater A function which updates the DOM (like attribute or text).
883
+ * @param node DOM Node updated when {@link module:utils/observablemixin~Observable} changes.
884
+ */
885
+ function syncValueSchemaValue(schema, updater, { node }) {
886
+ const values = getValueSchemaValue(schema, node);
887
+ let value;
888
+ // Check if schema is a single Template.bind.if, like:
889
+ //
890
+ // class: Template.bind.if( 'foo' )
891
+ //
892
+ if (schema.length == 1 && schema[0] instanceof TemplateIfBinding) {
893
+ value = values[0];
894
+ }
895
+ else {
896
+ value = values.reduce(arrayValueReducer, '');
897
+ }
898
+ if (isFalsy(value)) {
899
+ updater.remove();
900
+ }
901
+ else {
902
+ updater.set(value);
903
+ }
904
+ }
905
+ /**
906
+ * Returns an object consisting of `set` and `remove` functions, which
907
+ * can be used in the context of DOM Node to set or reset `textContent`.
908
+ * @see module:ui/view~View#_bindToObservable
909
+ *
910
+ * @param node DOM Node to be modified.
911
+ */
912
+ function getTextUpdater(node) {
913
+ return {
914
+ set(value) {
915
+ node.textContent = value;
916
+ },
917
+ remove() {
918
+ node.textContent = '';
919
+ }
920
+ };
921
+ }
922
+ /**
923
+ * Returns an object consisting of `set` and `remove` functions, which
924
+ * can be used in the context of DOM Node to set or reset an attribute.
925
+ * @see module:ui/view~View#_bindToObservable
926
+ *
927
+ * @param el DOM Node to be modified.
928
+ * @param attrName Name of the attribute to be modified.
929
+ * @param ns Namespace to use.
930
+ */
931
+ function getAttributeUpdater(el, attrName, ns) {
932
+ return {
933
+ set(value) {
934
+ el.setAttributeNS(ns, attrName, value);
935
+ },
936
+ remove() {
937
+ el.removeAttributeNS(ns, attrName);
938
+ }
939
+ };
940
+ }
941
+ /**
942
+ * Returns an object consisting of `set` and `remove` functions, which
943
+ * can be used in the context of CSSStyleDeclaration to set or remove a style.
944
+ * @see module:ui/view~View#_bindToObservable
945
+ *
946
+ * @param el DOM Node to be modified.
947
+ * @param styleName Name of the style to be modified.
948
+ */
949
+ function getStyleUpdater(el, styleName) {
950
+ return {
951
+ set(value) {
952
+ el.style[styleName] = value;
953
+ },
954
+ remove() {
955
+ el.style[styleName] = null;
956
+ }
957
+ };
958
+ }
959
+ /**
960
+ * Clones definition of the template.
961
+ */
962
+ function clone(def) {
963
+ const clone = cloneDeepWith(def, value => {
964
+ // Don't clone the `Template.bind`* bindings because of the references to Observable
965
+ // and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
966
+ // cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
967
+ // along with the definition.
968
+ //
969
+ // Don't clone Template instances if provided as a child. They're simply #render()ed
970
+ // and nothing should interfere.
971
+ //
972
+ // Also don't clone View instances if provided as a child of the Template. The template
973
+ // instance will be extracted from the View during the normalization and there's no need
974
+ // to clone it.
975
+ if (value && (value instanceof TemplateBinding || isTemplate(value) || isView(value) || isViewCollection(value))) {
976
+ return value;
977
+ }
978
+ });
979
+ return clone;
980
+ }
981
+ /**
982
+ * Normalizes given {@link module:ui/template~TemplateDefinition}.
983
+ *
984
+ * See:
985
+ * * {@link normalizeAttributes}
986
+ * * {@link normalizeListeners}
987
+ * * {@link normalizePlainTextDefinition}
988
+ * * {@link normalizeTextDefinition}
989
+ *
990
+ * @param def A template definition.
991
+ * @returns Normalized definition.
992
+ */
993
+ function normalize(def) {
994
+ if (typeof def == 'string') {
995
+ def = normalizePlainTextDefinition(def);
996
+ }
997
+ else if (def.text) {
998
+ normalizeTextDefinition(def);
999
+ }
1000
+ if (def.on) {
1001
+ def.eventListeners = normalizeListeners(def.on);
1002
+ // Template mixes EmitterMixin, so delete #on to avoid collision.
1003
+ delete def.on;
1004
+ }
1005
+ if (!def.text) {
1006
+ if (def.attributes) {
1007
+ normalizeAttributes(def.attributes);
1008
+ }
1009
+ const children = [];
1010
+ if (def.children) {
1011
+ if (isViewCollection(def.children)) {
1012
+ children.push(def.children);
1013
+ }
1014
+ else {
1015
+ for (const child of def.children) {
1016
+ if (isTemplate(child) || isView(child) || isNode(child)) {
1017
+ children.push(child);
1018
+ }
1019
+ else {
1020
+ children.push(new Template(child));
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ def.children = children;
1026
+ }
1027
+ return def;
1028
+ }
1029
+ /**
1030
+ * Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
1031
+ *
1032
+ * ```
1033
+ * attributes: {
1034
+ * a: 'bar',
1035
+ * b: {@link module:ui/template~TemplateBinding},
1036
+ * c: {
1037
+ * value: 'bar'
1038
+ * }
1039
+ * }
1040
+ * ```
1041
+ *
1042
+ * becomes
1043
+ *
1044
+ * ```
1045
+ * attributes: {
1046
+ * a: [ 'bar' ],
1047
+ * b: [ {@link module:ui/template~TemplateBinding} ],
1048
+ * c: {
1049
+ * value: [ 'bar' ]
1050
+ * }
1051
+ * }
1052
+ * ```
1053
+ */
1054
+ function normalizeAttributes(attributes) {
1055
+ for (const a in attributes) {
1056
+ if (attributes[a].value) {
1057
+ attributes[a].value = toArray(attributes[a].value);
1058
+ }
1059
+ arrayify(attributes, a);
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
1064
+ *
1065
+ * ```
1066
+ * on: {
1067
+ * a: 'bar',
1068
+ * b: {@link module:ui/template~TemplateBinding},
1069
+ * c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
1070
+ * }
1071
+ * ```
1072
+ *
1073
+ * becomes
1074
+ *
1075
+ * ```
1076
+ * on: {
1077
+ * a: [ 'bar' ],
1078
+ * b: [ {@link module:ui/template~TemplateBinding} ],
1079
+ * c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
1080
+ * }
1081
+ * ```
1082
+ *
1083
+ * @returns Object containing normalized listeners.
1084
+ */
1085
+ function normalizeListeners(listeners) {
1086
+ for (const l in listeners) {
1087
+ arrayify(listeners, l);
1088
+ }
1089
+ return listeners;
1090
+ }
1091
+ /**
1092
+ * Normalizes "string" {@link module:ui/template~TemplateDefinition}.
1093
+ *
1094
+ * ```
1095
+ * "foo"
1096
+ * ```
1097
+ *
1098
+ * becomes
1099
+ *
1100
+ * ```
1101
+ * { text: [ 'foo' ] },
1102
+ * ```
1103
+ *
1104
+ * @returns Normalized template definition.
1105
+ */
1106
+ function normalizePlainTextDefinition(def) {
1107
+ return {
1108
+ text: [def]
1109
+ };
1110
+ }
1111
+ /**
1112
+ * Normalizes text {@link module:ui/template~TemplateDefinition}.
1113
+ *
1114
+ * ```
1115
+ * children: [
1116
+ * { text: 'def' },
1117
+ * { text: {@link module:ui/template~TemplateBinding} }
1118
+ * ]
1119
+ * ```
1120
+ *
1121
+ * becomes
1122
+ *
1123
+ * ```
1124
+ * children: [
1125
+ * { text: [ 'def' ] },
1126
+ * { text: [ {@link module:ui/template~TemplateBinding} ] }
1127
+ * ]
1128
+ * ```
1129
+ */
1130
+ function normalizeTextDefinition(def) {
1131
+ def.text = toArray(def.text);
1132
+ }
1133
+ /**
1134
+ * Wraps an entry in Object in an Array, if not already one.
1135
+ *
1136
+ * ```
1137
+ * {
1138
+ * x: 'y',
1139
+ * a: [ 'b' ]
1140
+ * }
1141
+ * ```
1142
+ *
1143
+ * becomes
1144
+ *
1145
+ * ```
1146
+ * {
1147
+ * x: [ 'y' ],
1148
+ * a: [ 'b' ]
1149
+ * }
1150
+ * ```
1151
+ */
1152
+ function arrayify(obj, key) {
1153
+ obj[key] = toArray(obj[key]);
1154
+ }
1155
+ /**
1156
+ * A helper which concatenates the value avoiding unwanted
1157
+ * leading white spaces.
1158
+ */
1159
+ function arrayValueReducer(prev, cur) {
1160
+ if (isFalsy(cur)) {
1161
+ return prev;
1162
+ }
1163
+ else if (isFalsy(prev)) {
1164
+ return cur;
1165
+ }
1166
+ else {
1167
+ return `${prev} ${cur}`;
1168
+ }
1169
+ }
1170
+ /**
1171
+ * Extends one object defined in the following format:
1172
+ *
1173
+ * ```
1174
+ * {
1175
+ * key1: [Array1],
1176
+ * key2: [Array2],
1177
+ * ...
1178
+ * keyN: [ArrayN]
1179
+ * }
1180
+ * ```
1181
+ *
1182
+ * with another object of the same data format.
1183
+ *
1184
+ * @param obj Base object.
1185
+ * @param ext Object extending base.
1186
+ */
1187
+ function extendObjectValueArray(obj, ext) {
1188
+ for (const a in ext) {
1189
+ if (obj[a]) {
1190
+ obj[a].push(...ext[a]);
1191
+ }
1192
+ else {
1193
+ obj[a] = ext[a];
1194
+ }
1195
+ }
1196
+ }
1197
+ /**
1198
+ * A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
1199
+ * with content from {@link module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
1200
+ *
1201
+ * @param def A template instance to be extended.
1202
+ * @param def A definition which is to extend the template instance.
1203
+ * @param Error context.
1204
+ */
1205
+ function extendTemplate(template, def) {
1206
+ if (def.attributes) {
1207
+ if (!template.attributes) {
1208
+ template.attributes = {};
1209
+ }
1210
+ extendObjectValueArray(template.attributes, def.attributes);
1211
+ }
1212
+ if (def.eventListeners) {
1213
+ if (!template.eventListeners) {
1214
+ template.eventListeners = {};
1215
+ }
1216
+ extendObjectValueArray(template.eventListeners, def.eventListeners);
1217
+ }
1218
+ if (def.text) {
1219
+ template.text.push(...def.text);
1220
+ }
1221
+ if (def.children && def.children.length) {
1222
+ if (template.children.length != def.children.length) {
1223
+ /**
1224
+ * The number of children in extended definition does not match.
1225
+ *
1226
+ * @error ui-template-extend-children-mismatch
1227
+ */
1228
+ throw new CKEditorError('ui-template-extend-children-mismatch', template);
1229
+ }
1230
+ let childIndex = 0;
1231
+ for (const childDef of def.children) {
1232
+ extendTemplate(template.children[childIndex++], childDef);
1233
+ }
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Checks if value is "falsy".
1238
+ * Note: 0 (Number) is not "falsy" in this context.
1239
+ *
1240
+ * @param value Value to be checked.
1241
+ */
1242
+ function isFalsy(value) {
1243
+ return !value && value !== 0;
1244
+ }
1245
+ /**
1246
+ * Checks if the item is an instance of {@link module:ui/view~View}
1247
+ *
1248
+ * @param value Value to be checked.
1249
+ */
1250
+ function isView(item) {
1251
+ return item instanceof View;
1252
+ }
1253
+ /**
1254
+ * Checks if the item is an instance of {@link module:ui/template~Template}
1255
+ *
1256
+ * @param value Value to be checked.
1257
+ */
1258
+ function isTemplate(item) {
1259
+ return item instanceof Template;
1260
+ }
1261
+ /**
1262
+ * Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
1263
+ *
1264
+ * @param value Value to be checked.
1265
+ */
1266
+ function isViewCollection(item) {
1267
+ return item instanceof ViewCollection;
1268
+ }
1269
+ /**
1270
+ * Checks if value array contains the one with namespace.
1271
+ */
1272
+ function isNamespaced(attrValue) {
1273
+ return isObject(attrValue[0]) && attrValue[0].ns;
1274
+ }
1275
+ /**
1276
+ * Creates an empty skeleton for {@link module:ui/template~Template#revert}
1277
+ * data.
1278
+ */
1279
+ function getEmptyRevertData() {
1280
+ return {
1281
+ children: [],
1282
+ bindings: [],
1283
+ attributes: {}
1284
+ };
1285
+ }
1286
+ /**
1287
+ * Checks whether an attribute should be extended when
1288
+ * {@link module:ui/template~Template#apply} is called.
1289
+ *
1290
+ * @param attrName Attribute name to check.
1291
+ */
1292
+ function shouldExtend(attrName) {
1293
+ return attrName == 'class' || attrName == 'style';
1294
+ }