1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* FluentDOM\Query implements a jQuery like replacement for DOMNodeList |
4
|
|
|
* |
5
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php The MIT License |
6
|
|
|
* @copyright Copyright (c) 2009-2017 FluentDOM Contributors |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace FluentDOM { |
10
|
|
|
|
11
|
|
|
use FluentDOM\Nodes\Fetcher; |
12
|
|
|
use FluentDOM\Utility\Constraints; |
13
|
|
|
use FluentDOM\Utility\QualifiedName; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* FluentDOM\Query implements a jQuery like replacement for DOMNodeList |
17
|
|
|
* |
18
|
|
|
* @property Query\Attributes $attr |
19
|
|
|
* @property Query\Data $data |
20
|
|
|
* @property Query\Css $css |
21
|
|
|
* |
22
|
|
|
* Define methods from parent to provide information for code completion |
23
|
|
|
* @method Query load($source, string $contentType = NULL, $options = []) |
24
|
|
|
* @method Query spawn($elements = NULL) |
25
|
|
|
* @method Query find($selector, int $options = 0) |
26
|
|
|
* @method Query end() |
27
|
|
|
* @method Query formatOutput(string $contentType = NULL) |
28
|
|
|
*/ |
29
|
|
|
class Query extends Nodes { |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var Nodes\Builder |
33
|
|
|
*/ |
34
|
|
|
private $_builder; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Virtual properties, validate existence |
38
|
|
|
* |
39
|
|
|
* @param string $name |
40
|
|
|
* @return bool |
41
|
|
|
*/ |
42
|
4 |
|
public function __isset(string $name): bool { |
43
|
|
|
switch ($name) { |
44
|
4 |
|
case 'attr' : |
45
|
3 |
|
case 'css' : |
46
|
2 |
|
case 'data' : |
47
|
3 |
|
return TRUE; |
48
|
|
|
} |
49
|
1 |
|
return parent::__isset($name); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Virtual properties, read property |
54
|
|
|
* |
55
|
|
|
* @param string $name |
56
|
|
|
* @throws \UnexpectedValueException |
57
|
|
|
* @return mixed |
58
|
|
|
* @throws \LogicException |
59
|
|
|
*/ |
60
|
57 |
|
public function __get(string $name) { |
61
|
|
|
switch ($name) { |
62
|
57 |
|
case 'attr' : |
63
|
1 |
|
return new Query\Attributes($this); |
64
|
56 |
|
case 'css' : |
65
|
1 |
|
return new Query\Css($this); |
66
|
55 |
|
case 'data' : |
67
|
2 |
|
if ($node = $this->getFirstElement()) { |
68
|
1 |
|
return new Query\Data($node); |
69
|
|
|
} else { |
70
|
1 |
|
throw new \UnexpectedValueException( |
71
|
1 |
|
'UnexpectedValueException: first selected node is no element.' |
72
|
|
|
); |
73
|
|
|
} |
74
|
|
|
} |
75
|
53 |
|
return parent::__get($name); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Block changing the readonly dynamic property |
80
|
|
|
* |
81
|
|
|
* @param string $name |
82
|
|
|
* @param mixed $value |
83
|
|
|
* @throws \InvalidArgumentException |
84
|
|
|
* @throws \BadMethodCallException |
85
|
|
|
*/ |
86
|
5 |
|
public function __set(string $name, $value) { |
87
|
|
|
switch ($name) { |
88
|
5 |
|
case 'attr' : |
89
|
2 |
|
$this->attr( |
90
|
2 |
|
$value instanceOf Query\Attributes ? $value->toArray() : $value |
91
|
|
|
); |
92
|
2 |
|
break; |
93
|
3 |
|
case 'css' : |
94
|
2 |
|
$this->css($value); |
95
|
2 |
|
break; |
96
|
1 |
|
case 'data' : |
97
|
1 |
|
$this->data( |
98
|
1 |
|
$value instanceOf Query\Data ? $value->toArray() : $value |
99
|
|
|
); |
100
|
1 |
|
break; |
101
|
|
|
} |
102
|
5 |
|
parent::__set($name, $value); |
103
|
5 |
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Throws an exception if somebody tries to unset one |
107
|
|
|
* of the dynamic properties |
108
|
|
|
* |
109
|
|
|
* @param string $name |
110
|
|
|
* @throws \BadMethodCallException |
111
|
|
|
*/ |
112
|
4 |
|
public function __unset(string $name) { |
113
|
|
|
switch ($name) { |
114
|
4 |
|
case 'attr' : |
115
|
3 |
|
case 'css' : |
116
|
2 |
|
case 'data' : |
117
|
3 |
|
throw new \BadMethodCallException( |
118
|
3 |
|
sprintf( |
119
|
3 |
|
'Can not unset property %s::$%s', |
120
|
3 |
|
get_class($this), |
121
|
3 |
|
$name |
122
|
|
|
) |
123
|
|
|
); |
124
|
|
|
} |
125
|
1 |
|
parent::__unset($name); |
126
|
|
|
// @codeCoverageIgnoreStart |
127
|
|
|
} |
128
|
|
|
// @codeCoverageIgnoreEnd |
129
|
|
|
|
130
|
|
|
/****************** |
131
|
|
|
* Internal |
132
|
|
|
*****************/ |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Returns the item from the internal array if |
136
|
|
|
* if the index exists and is an DOMElement |
137
|
|
|
* |
138
|
|
|
* @param array|\Traversable |
139
|
|
|
* @return NULL|\DOMElement |
140
|
|
|
*/ |
141
|
4 |
|
private function getFirstElement() { |
142
|
4 |
|
foreach ($this->_nodes as $node) { |
143
|
4 |
|
if ($node instanceof \DOMElement) { |
144
|
3 |
|
return $node; |
145
|
|
|
} |
146
|
|
|
} |
147
|
1 |
|
return NULL; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Wrap $content around a set of elements |
152
|
|
|
* |
153
|
|
|
* @param array $elements |
154
|
|
|
* @param string|array|\DOMNode|\Traversable|callable $content |
155
|
|
|
* @return array |
156
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
157
|
|
|
* @throws \InvalidArgumentException |
158
|
|
|
*/ |
159
|
8 |
|
private function wrapNodes(array $elements, $content): array { |
160
|
8 |
|
$result = []; |
161
|
8 |
|
$wrapperTemplate = NULL; |
162
|
8 |
|
$callback = Constraints::filterCallable($content); |
163
|
8 |
|
if (!$callback) { |
164
|
6 |
|
$wrapperTemplate = $this->build()->getContentElement($content); |
165
|
|
|
} |
166
|
7 |
|
$simple = FALSE; |
167
|
7 |
|
foreach ($elements as $index => $node) { |
168
|
7 |
|
if ($callback) { |
169
|
2 |
|
$wrapperTemplate = NULL; |
170
|
2 |
|
$wrapContent = $callback($node, $index); |
171
|
2 |
|
if (!empty($wrapContent)) { |
172
|
2 |
|
$wrapperTemplate = $this->build()->getContentElement($wrapContent); |
173
|
|
|
} |
174
|
|
|
} |
175
|
7 |
|
if ($wrapperTemplate instanceof \DOMElement) { |
176
|
|
|
/** |
177
|
|
|
* @var \DOMElement $target |
178
|
|
|
* @var \DOMElement $wrapper |
179
|
|
|
*/ |
180
|
7 |
|
list($target, $wrapper) = $this->build()->getWrapperNodes( |
181
|
7 |
|
$wrapperTemplate, |
182
|
7 |
|
$simple |
183
|
|
|
); |
184
|
7 |
|
if ($node->parentNode instanceof \DOMNode) { |
185
|
7 |
|
$node->parentNode->insertBefore($wrapper, $node); |
186
|
|
|
} |
187
|
7 |
|
$target->appendChild($node); |
188
|
7 |
|
$result[] = $node; |
189
|
|
|
} |
190
|
|
|
} |
191
|
7 |
|
return $result; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/********************* |
195
|
|
|
* Core |
196
|
|
|
********************/ |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @param \DOMNode $node |
200
|
|
|
* @return Nodes\Modifier |
201
|
|
|
*/ |
202
|
32 |
|
private function modify(\DOMNode $node): Nodes\Modifier { |
203
|
32 |
|
return new Nodes\Modifier($node); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @return Nodes\Builder |
208
|
|
|
*/ |
209
|
57 |
|
private function build(): Nodes\Builder { |
210
|
57 |
|
if (NULL === $this->_builder) { |
211
|
57 |
|
$this->_builder = new Nodes\Builder($this); |
212
|
|
|
} |
213
|
57 |
|
return $this->_builder; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Use a handler callback to apply a content argument to each node $targetNodes. The content |
218
|
|
|
* argument can be an easy setter function |
219
|
|
|
* |
220
|
|
|
* @param array|\DOMNodeList $targetNodes |
221
|
|
|
* @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
222
|
|
|
* @param callable $handler |
223
|
|
|
* @return array |
224
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
225
|
|
|
* @throws \InvalidArgumentException |
226
|
|
|
*/ |
227
|
21 |
|
private function apply($targetNodes, $content, callable $handler): array { |
228
|
21 |
|
$result = []; |
229
|
21 |
|
$isSetterFunction = FALSE; |
230
|
21 |
|
if ($callback = Constraints::filterCallable($content)) { |
231
|
6 |
|
$isSetterFunction = TRUE; |
232
|
|
|
} else { |
233
|
15 |
|
$contentNodes = $this->build()->getContentNodes($content); |
234
|
|
|
} |
235
|
21 |
|
foreach ($targetNodes as $index => $node) { |
236
|
21 |
|
if ($isSetterFunction) { |
237
|
6 |
|
$contentData = $callback($node, $index, $this->build()->getInnerXml($node)); |
238
|
6 |
|
if (!empty($contentData)) { |
239
|
6 |
|
$contentNodes = $this->build()->getContentNodes($contentData); |
240
|
|
|
} |
241
|
|
|
} |
242
|
21 |
|
if (!empty($contentNodes)) { |
243
|
21 |
|
$resultNodes = $handler($node, $contentNodes); |
244
|
21 |
|
if (is_array($resultNodes)) { |
245
|
21 |
|
$result = array_merge($result, $resultNodes); |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
} |
249
|
21 |
|
return $result; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Apply the content to the target nodes using the handler callback |
254
|
|
|
* and push them into a spawned Query object. |
255
|
|
|
* |
256
|
|
|
* @param array|\DOMNodeList $targetNodes |
257
|
|
|
* @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
258
|
|
|
* @param callable $handler |
259
|
|
|
* @param bool $remove Call remove() on $this, remove the current selection from the DOM |
260
|
|
|
* @return Query |
261
|
|
|
*/ |
262
|
19 |
|
private function applyToSpawn($targetNodes, $content, callable $handler, bool $remove = FALSE): Query { |
263
|
19 |
|
$result = $this->spawn( |
264
|
19 |
|
$this->apply($targetNodes, $content, $handler) |
265
|
|
|
); |
266
|
19 |
|
if ($remove) { |
267
|
7 |
|
$this->remove(); |
268
|
|
|
} |
269
|
19 |
|
return $result; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Apply the handler the $handler to nodes defined by selector, using |
274
|
|
|
* the currently selected nodes as context. |
275
|
|
|
* |
276
|
|
|
* @param string|array|\DOMNode|\Traversable $selector |
277
|
|
|
* @param callable $handler |
278
|
|
|
* @param bool $remove Call remove() on $this, remove the current selection from the DOM |
279
|
|
|
* @return Query |
280
|
|
|
*/ |
281
|
4 |
|
private function applyToSelector($selector, callable $handler, bool $remove = FALSE): Query { |
282
|
4 |
|
return $this->applyToSpawn( |
283
|
4 |
|
$this->build()->getTargetNodes($selector), |
284
|
4 |
|
$this->_nodes, |
285
|
4 |
|
$handler, |
286
|
4 |
|
$remove |
287
|
|
|
); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/********************* |
291
|
|
|
* Traversing |
292
|
|
|
********************/ |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Adds more elements, matched by the given expression, to the set of matched elements. |
296
|
|
|
* |
297
|
|
|
* @example add.php Usage Examples: FluentDOM::add() |
298
|
|
|
* @param string|\Traversable|array $selector selector |
299
|
|
|
* @param array|\Traversable $context |
300
|
|
|
* @return Query |
301
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
302
|
|
|
* @throws \OutOfBoundsException |
303
|
|
|
* @throws \InvalidArgumentException |
304
|
|
|
*/ |
305
|
7 |
|
public function add($selector, $context = NULL) { |
306
|
7 |
|
$result = $this->spawn($this); |
307
|
7 |
|
if (NULL !== $context) { |
308
|
1 |
|
$result->push($this->spawn($context)->find($selector)); |
309
|
|
|
} elseif ( |
310
|
6 |
|
is_object($selector) || |
311
|
5 |
|
(is_string($selector) && substr(ltrim($selector), 0, 1) === '<') |
312
|
|
|
) { |
313
|
4 |
|
$result->push($this->build()->getContentNodes($selector)); |
314
|
|
|
} else { |
315
|
2 |
|
$result->push($this->find($selector)); |
316
|
|
|
} |
317
|
7 |
|
$result->_nodes = $result->unique($result->_nodes); |
318
|
7 |
|
return $result; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Add the previous selection to the current selection. |
323
|
|
|
* |
324
|
|
|
* @return Query |
325
|
|
|
* @throws \OutOfBoundsException |
326
|
|
|
* @throws \InvalidArgumentException |
327
|
|
|
*/ |
328
|
1 |
|
public function addBack(): Query { |
329
|
1 |
|
return $this->spawn()->push($this->_nodes)->push($this->_parent); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Get a set of elements containing of the unique immediate |
334
|
|
|
* child nodes including only elements (not text nodes) of each |
335
|
|
|
* of the matched set of elements. |
336
|
|
|
* |
337
|
|
|
* @example children.php Usage Examples: FluentDOM\Query::children() |
338
|
|
|
* @param string $selector selector |
339
|
|
|
* @return Query|Nodes |
340
|
|
|
* @throws \OutOfBoundsException |
341
|
|
|
* @throws \LogicException |
342
|
|
|
* @throws \InvalidArgumentException |
343
|
|
|
*/ |
344
|
2 |
|
public function children($selector = NULL): Query { |
345
|
2 |
|
return $this->fetch('*', $selector, NULL, Nodes\Fetcher::UNIQUE); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Get a set of elements containing the closest parent element that matches the specified |
350
|
|
|
* selector, the starting element included. |
351
|
|
|
* |
352
|
|
|
* @example closest.php Usage Example: FluentDOM\Query::closest() |
353
|
|
|
* @param string $selector selector |
354
|
|
|
* @param array|\Traversable $context |
355
|
|
|
* @return Query|Nodes |
356
|
|
|
* @throws \OutOfBoundsException |
357
|
|
|
* @throws \LogicException |
358
|
|
|
* @throws \InvalidArgumentException |
359
|
|
|
*/ |
360
|
4 |
|
public function closest($selector, $context = NULL): Query { |
361
|
4 |
|
$context = $context ? $this->spawn($context) : $this; |
362
|
4 |
|
return $context->fetch( |
363
|
4 |
|
'ancestor-or-self::*', |
364
|
4 |
|
$selector, |
365
|
4 |
|
$selector, |
366
|
4 |
|
Fetcher::REVERSE |Fetcher::INCLUDE_STOP |
367
|
|
|
); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Get a set of elements containing all of the unique immediate |
372
|
|
|
* child nodes including elements and text nodes of each of the matched set of elements. |
373
|
|
|
* |
374
|
|
|
* @return Query|Nodes |
375
|
|
|
* @throws \OutOfBoundsException |
376
|
|
|
* @throws \LogicException |
377
|
|
|
* @throws \InvalidArgumentException |
378
|
|
|
*/ |
379
|
1 |
|
public function contents(): Query { |
380
|
1 |
|
return $this->fetch( |
381
|
1 |
|
'*|text()[normalize-space(.) != ""]', |
382
|
1 |
|
NULL, |
383
|
1 |
|
NULL, |
384
|
1 |
|
Fetcher::UNIQUE |
385
|
|
|
); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Reduce the set of matched elements to a single element. |
390
|
|
|
* |
391
|
|
|
* @example eq.php Usage Example: FluentDOM\Query::eq() |
392
|
|
|
* @param int $position Element index (start with 0) |
393
|
|
|
* @return Query |
394
|
|
|
* @throws \OutOfBoundsException |
395
|
|
|
* @throws \InvalidArgumentException |
396
|
|
|
*/ |
397
|
2 |
|
public function eq(int $position): Query { |
398
|
2 |
|
$result = $this->spawn(); |
399
|
2 |
|
if ($position < 0) { |
400
|
1 |
|
$position = count($this->_nodes) + $position; |
401
|
|
|
} |
402
|
2 |
|
if (isset($this->_nodes[$position])) { |
403
|
2 |
|
$result->push($this->_nodes[$position]); |
404
|
|
|
} |
405
|
2 |
|
return $result; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Removes all elements from the set of matched elements that do not match |
410
|
|
|
* the specified expression(s). |
411
|
|
|
* |
412
|
|
|
* @example filter-expr.php Usage Example: FluentDOM\Query::filter() with selector |
413
|
|
|
* @example filter-fn.php Usage Example: FluentDOM\Query::filter() with Closure |
414
|
|
|
* @param string|callable $selector selector or callback function |
415
|
|
|
* @return Query |
416
|
|
|
* @throws \OutOfBoundsException |
417
|
|
|
* @throws \InvalidArgumentException |
418
|
|
|
*/ |
419
|
2 |
|
public function filter($selector): Query { |
420
|
2 |
|
$callback = $this->getSelectorCallback($selector); |
421
|
2 |
|
$result = $this->spawn(); |
422
|
2 |
|
foreach ($this->_nodes as $index => $node) { |
423
|
2 |
|
if ($callback($node, $index)) { |
424
|
2 |
|
$result->push($node); |
425
|
|
|
} |
426
|
|
|
} |
427
|
2 |
|
return $result; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Get a set of elements containing only the first of the currently selected elements. |
432
|
|
|
* |
433
|
|
|
* @return Query |
434
|
|
|
* @throws \OutOfBoundsException |
435
|
|
|
* @throws \InvalidArgumentException |
436
|
|
|
*/ |
437
|
1 |
|
public function first(): Query { |
438
|
1 |
|
return $this->eq(0); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Retrieve the matched DOM elements in an array. A negative position will be counted from the end. |
443
|
|
|
* |
444
|
|
|
* @param int|NULL optional offset of a single element to get. |
445
|
|
|
* @return array|\DOMNode|NULL |
446
|
|
|
*/ |
447
|
4 |
|
public function get(int $position = NULL) { |
448
|
4 |
|
if (NULL === $position) { |
449
|
1 |
|
return $this->_nodes; |
450
|
|
|
} |
451
|
3 |
|
if ($position < 0) { |
452
|
1 |
|
$position = count($this->_nodes) + $position; |
453
|
|
|
} |
454
|
3 |
|
if (isset($this->_nodes[$position])) { |
455
|
2 |
|
return $this->_nodes[$position]; |
456
|
|
|
} |
457
|
1 |
|
return NULL; |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
/** |
461
|
|
|
* Reduce the set of matched elements to those that have |
462
|
|
|
* a descendant that matches the selector or DOM element. |
463
|
|
|
* |
464
|
|
|
* @param string|\DOMNode $selector selector or DOMNode |
465
|
|
|
* @return Query |
466
|
|
|
* @throws \OutOfBoundsException |
467
|
|
|
* @throws \InvalidArgumentException |
468
|
|
|
*/ |
469
|
2 |
|
public function has($selector): Query { |
470
|
2 |
|
$callback = $this->getSelectorCallback($selector); |
471
|
2 |
|
$result = $this->spawn(); |
472
|
2 |
|
$expression = './/node()'; |
473
|
2 |
|
if ($selector instanceof \DOMElement) { |
474
|
1 |
|
$expression = './/*'; |
475
|
|
|
} |
476
|
2 |
|
foreach ($this->_nodes as $node) { |
477
|
2 |
|
foreach ($this->xpath($expression, $node) as $has) { |
478
|
2 |
|
if ($callback($has)) { |
479
|
2 |
|
$result->push($node); |
480
|
2 |
|
break; |
481
|
|
|
} |
482
|
|
|
} |
483
|
|
|
} |
484
|
2 |
|
return $result; |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Checks the current selection against an expression and returns TRUE, |
489
|
|
|
* if at least one element of the selection fits the given expression. |
490
|
|
|
* |
491
|
|
|
* @example is.php Usage Example: FluentDOM\Query::is() |
492
|
|
|
* @param string $selector selector |
493
|
|
|
* @return bool |
494
|
|
|
*/ |
495
|
2 |
|
public function is(string $selector): bool { |
496
|
2 |
|
foreach ($this->_nodes as $node) { |
497
|
1 |
|
if ($this->matches($selector, $node)) { |
498
|
1 |
|
return TRUE; |
499
|
|
|
} |
500
|
|
|
} |
501
|
2 |
|
return FALSE; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
/** |
505
|
|
|
* Get a set of elements containing only the last of the currently selected elements. |
506
|
|
|
* |
507
|
|
|
* @return Query |
508
|
|
|
* @throws \OutOfBoundsException |
509
|
|
|
* @throws \InvalidArgumentException |
510
|
|
|
*/ |
511
|
1 |
|
public function last(): Query { |
512
|
1 |
|
return $this->eq(-1); |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
/** |
516
|
|
|
* Translate a set of elements in the FluentDOM\Query object into |
517
|
|
|
* another set of values in an array (which may, or may not contain elements). |
518
|
|
|
* |
519
|
|
|
* If the callback function returns an array each element of the array will be added to the |
520
|
|
|
* result array. All other variable types are put directly into the result array. |
521
|
|
|
* |
522
|
|
|
* @example map.php Usage Example: FluentDOM\Query::map() |
523
|
|
|
* @param callable $function |
524
|
|
|
* @return array |
525
|
|
|
*/ |
526
|
2 |
|
public function map(callable $function): array { |
527
|
2 |
|
$result = []; |
528
|
2 |
|
foreach ($this->_nodes as $index => $node) { |
529
|
2 |
|
$mapped = $function($node, $index); |
530
|
2 |
|
if ($mapped === NULL) { |
531
|
1 |
|
continue; |
532
|
|
|
} |
533
|
2 |
|
if ($mapped instanceof \Traversable || is_array($mapped)) { |
534
|
1 |
|
foreach ($mapped as $element) { |
535
|
1 |
|
if ($element !== NULL) { |
536
|
1 |
|
$result[] = $element; |
537
|
|
|
} |
538
|
|
|
} |
539
|
|
|
} else { |
540
|
2 |
|
$result[] = $mapped; |
541
|
|
|
} |
542
|
|
|
} |
543
|
2 |
|
return $result; |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* Removes elements matching the specified expression from the set of matched elements. |
548
|
|
|
* |
549
|
|
|
* @example not.php Usage Example: FluentDOM\Query::not() |
550
|
|
|
* @param string|callback $selector selector or callback function |
551
|
|
|
* @return Query |
552
|
|
|
* @throws \OutOfBoundsException |
553
|
|
|
* @throws \InvalidArgumentException |
554
|
|
|
*/ |
555
|
2 |
|
public function not($selector): Query { |
556
|
2 |
|
$callback = $this->getSelectorCallback($selector); |
557
|
2 |
|
return $this->filter( |
558
|
|
|
function (\DOMNode $node, $index) use ($callback) { |
559
|
2 |
|
return !$callback($node, $index); |
560
|
2 |
|
} |
561
|
|
|
); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Get a set of elements containing the unique next siblings of each of the |
566
|
|
|
* given set of elements. |
567
|
|
|
* |
568
|
|
|
* @example next.php Usage Example: FluentDOM\Query::next() |
569
|
|
|
* @param string $selector |
570
|
|
|
* @return Query|Nodes |
571
|
|
|
* @throws \OutOfBoundsException |
572
|
|
|
* @throws \LogicException |
573
|
|
|
* @throws \InvalidArgumentException |
574
|
|
|
*/ |
575
|
1 |
|
public function next($selector = NULL): Query { |
576
|
1 |
|
return $this->fetch( |
577
|
1 |
|
'following-sibling::node()[ |
578
|
|
|
self::* or (self::text() and normalize-space(.) != "") |
579
|
|
|
][1]', |
580
|
1 |
|
$selector, |
581
|
1 |
|
NULL, |
582
|
1 |
|
Nodes\Fetcher::UNIQUE |
583
|
|
|
); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
/** |
587
|
|
|
* Find all sibling elements after the current element. |
588
|
|
|
* |
589
|
|
|
* @example nextAll.php Usage Example: FluentDOM\Query::nextAll() |
590
|
|
|
* @param string $selector selector |
591
|
|
|
* @return Query|Nodes |
592
|
|
|
* @throws \OutOfBoundsException |
593
|
|
|
* @throws \LogicException |
594
|
|
|
* @throws \InvalidArgumentException |
595
|
|
|
*/ |
596
|
1 |
|
public function nextAll($selector = NULL): Query { |
597
|
1 |
|
return $this->fetch( |
598
|
1 |
|
'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]', |
599
|
1 |
|
$selector |
600
|
|
|
); |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
/** |
604
|
|
|
* Get all following siblings of each element up to but |
605
|
|
|
* not including the element matched by the selector. |
606
|
|
|
* |
607
|
|
|
* @param string $selector selector |
608
|
|
|
* @param string $filter selector |
609
|
|
|
* @return Query|Nodes |
610
|
|
|
* @throws \OutOfBoundsException |
611
|
|
|
* @throws \LogicException |
612
|
|
|
* @throws \InvalidArgumentException |
613
|
|
|
*/ |
614
|
1 |
|
public function nextUntil($selector = NULL, $filter = NULL): Query { |
615
|
1 |
|
return $this->fetch( |
616
|
1 |
|
'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]', |
617
|
1 |
|
$filter, |
618
|
1 |
|
$selector |
619
|
|
|
); |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
/** |
623
|
|
|
* Get a set of elements containing the unique parents of the matched set of elements. |
624
|
|
|
* |
625
|
|
|
* @example parent.php Usage Example: FluentDOM\Query::parent() |
626
|
|
|
* @return Query|Nodes |
627
|
|
|
* @throws \OutOfBoundsException |
628
|
|
|
* @throws \LogicException |
629
|
|
|
* @throws \InvalidArgumentException |
630
|
|
|
*/ |
631
|
1 |
|
public function parent(): Query { |
632
|
1 |
|
return $this->fetch('parent::*', NULL, NULL, Fetcher::UNIQUE); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* Get the ancestors of each element in the current set of matched elements, |
637
|
|
|
* optionally filtered by a selector. |
638
|
|
|
* |
639
|
|
|
* @example parents.php Usage Example: FluentDOM\Query::parents() |
640
|
|
|
* @param string $selector selector |
641
|
|
|
* @return Query|Nodes |
642
|
|
|
* @throws \LogicException |
643
|
|
|
* @throws \OutOfBoundsException |
644
|
|
|
* @throws \InvalidArgumentException |
645
|
|
|
*/ |
646
|
1 |
|
public function parents($selector = NULL): Query { |
647
|
1 |
|
return $this->fetch('ancestor::*', $selector, NULL, Fetcher::REVERSE); |
648
|
|
|
} |
649
|
|
|
|
650
|
|
|
/** |
651
|
|
|
* Get the ancestors of each element in the current set of matched elements, |
652
|
|
|
* up to but not including the element matched by the selector. |
653
|
|
|
* |
654
|
|
|
* @param string $stopAt selector |
655
|
|
|
* @param string $filter selector |
656
|
|
|
* @return Query|Nodes |
657
|
|
|
* @throws \OutOfBoundsException |
658
|
|
|
* @throws \LogicException |
659
|
|
|
* @throws \InvalidArgumentException |
660
|
|
|
*/ |
661
|
1 |
|
public function parentsUntil($stopAt = NULL, $filter = NULL): Query { |
662
|
1 |
|
return $this->fetch('ancestor::*', $filter, $stopAt,Nodes\Fetcher::REVERSE); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Get a set of elements containing the unique previous siblings of each of the |
667
|
|
|
* matched set of elements. |
668
|
|
|
* |
669
|
|
|
* @example prev.php Usage Example: FluentDOM\Query::prev() |
670
|
|
|
* @param string $selector selector |
671
|
|
|
* @return Query|Nodes |
672
|
|
|
* @throws \OutOfBoundsException |
673
|
|
|
* @throws \LogicException |
674
|
|
|
* @throws \InvalidArgumentException |
675
|
|
|
*/ |
676
|
2 |
|
public function prev($selector = NULL): Query { |
677
|
2 |
|
return $this->fetch( |
678
|
2 |
|
'preceding-sibling::node()[ |
679
|
|
|
self::* or (self::text() and normalize-space(.) != "") |
680
|
|
|
][1]', |
681
|
2 |
|
$selector, |
682
|
2 |
|
NULL, |
683
|
2 |
|
Nodes\Fetcher::UNIQUE |
684
|
|
|
); |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Find all sibling elements in front of the current element. |
689
|
|
|
* |
690
|
|
|
* @example prevAll.php Usage Example: FluentDOM\Query::prevAll() |
691
|
|
|
* @param string $selector selector |
692
|
|
|
* @return Query|Nodes |
693
|
|
|
* @throws \OutOfBoundsException |
694
|
|
|
* @throws \LogicException |
695
|
|
|
* @throws \InvalidArgumentException |
696
|
|
|
*/ |
697
|
1 |
|
public function prevAll($selector = NULL): Query { |
698
|
1 |
|
return $this->fetch( |
699
|
1 |
|
'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]', |
700
|
1 |
|
$selector, |
701
|
1 |
|
NULL, |
702
|
1 |
|
Nodes\Fetcher::REVERSE |
703
|
|
|
); |
704
|
|
|
} |
705
|
|
|
|
706
|
|
|
/** |
707
|
|
|
* Get all preceding siblings of each element up to but not including |
708
|
|
|
* the element matched by the selector. |
709
|
|
|
* |
710
|
|
|
* @param string $selector selector |
711
|
|
|
* @param string $filter selector |
712
|
|
|
* @return Query|Nodes |
713
|
|
|
* @throws \OutOfBoundsException |
714
|
|
|
* @throws \LogicException |
715
|
|
|
* @throws \InvalidArgumentException |
716
|
|
|
*/ |
717
|
1 |
|
public function prevUntil($selector = NULL, $filter = NULL): Query { |
718
|
1 |
|
return $this->fetch( |
719
|
1 |
|
'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]', |
720
|
1 |
|
$filter, |
721
|
1 |
|
$selector, |
722
|
1 |
|
Nodes\Fetcher::REVERSE |
723
|
|
|
); |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
|
727
|
|
|
/** |
728
|
|
|
* Reverse the order of the matched elements. |
729
|
|
|
* |
730
|
|
|
* @return Query |
731
|
|
|
* @throws \OutOfBoundsException |
732
|
|
|
* @throws \InvalidArgumentException |
733
|
|
|
*/ |
734
|
1 |
|
public function reverse(): Query { |
735
|
1 |
|
return $this->spawn()->push(array_reverse($this->_nodes)); |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* Get a set of elements containing all of the unique siblings of each of the |
740
|
|
|
* matched set of elements. |
741
|
|
|
* |
742
|
|
|
* @example siblings.php Usage Example: FluentDOM\Query::siblings() |
743
|
|
|
* @param string $selector selector |
744
|
|
|
* @return Query|Nodes |
745
|
|
|
* @throws \OutOfBoundsException |
746
|
|
|
* @throws \LogicException |
747
|
|
|
* @throws \InvalidArgumentException |
748
|
|
|
*/ |
749
|
1 |
|
public function siblings($selector = NULL): Query { |
750
|
1 |
|
return $this->fetch( |
751
|
1 |
|
'preceding-sibling::*| |
752
|
|
|
preceding-sibling::text()[normalize-space(.) != ""]| |
753
|
|
|
following-sibling::*| |
754
|
|
|
following-sibling::text()[normalize-space(.) != ""]', |
755
|
1 |
|
$selector, |
756
|
1 |
|
NULL, |
757
|
1 |
|
Nodes\Fetcher::REVERSE |
758
|
|
|
); |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Selects a subset of the matched elements. |
763
|
|
|
* |
764
|
|
|
* @example slice.php Usage Example: FluentDOM\Query::slice() |
765
|
|
|
* @param int $start |
766
|
|
|
* @param int $end |
767
|
|
|
* @return Query |
768
|
|
|
* @throws \OutOfBoundsException |
769
|
|
|
* @throws \InvalidArgumentException |
770
|
|
|
*/ |
771
|
4 |
|
public function slice(int $start, int $end = NULL): Query { |
772
|
4 |
|
$result = $this->spawn(); |
773
|
4 |
|
if ($end === NULL) { |
774
|
1 |
|
$result->push(array_slice($this->_nodes, $start)); |
775
|
3 |
|
} elseif ($end < 0) { |
776
|
1 |
|
$result->push(array_slice($this->_nodes, $start, $end)); |
777
|
2 |
|
} elseif ($end > $start) { |
778
|
1 |
|
$result->push(array_slice($this->_nodes, $start, $end - $start)); |
779
|
|
|
} else { |
780
|
1 |
|
$result->push(array_slice($this->_nodes, $end, $start - $end)); |
781
|
|
|
} |
782
|
4 |
|
return $result; |
783
|
|
|
} |
784
|
|
|
|
785
|
|
|
/********************* |
786
|
|
|
* Manipulation |
787
|
|
|
********************/ |
788
|
|
|
|
789
|
|
|
/** |
790
|
|
|
* Insert content after each of the matched elements. |
791
|
|
|
* |
792
|
|
|
* @example after.php Usage Example: FluentDOM\Query::after() |
793
|
|
|
* @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
794
|
|
|
* @return Query |
795
|
|
|
*/ |
796
|
2 |
|
public function after($content): Query { |
797
|
2 |
|
return $this->applyToSpawn( |
798
|
2 |
|
$this->_nodes, |
799
|
2 |
|
$content, |
800
|
|
|
function($targetNode, $contentNodes) { |
801
|
2 |
|
return $this->modify($targetNode)->insertNodesAfter($contentNodes); |
802
|
2 |
|
} |
803
|
|
|
); |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* Append content to the inside of every matched element. |
808
|
|
|
* |
809
|
|
|
* @example append.php Usage Example: FluentDOM\Query::append() |
810
|
|
|
* @param string|array|\DOMNode|\Traversable|callable $content DOMNode or DOMNodeList or xml fragment string |
811
|
|
|
* @return Query |
812
|
|
|
* @throws \InvalidArgumentException |
813
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
814
|
|
|
* @throws \LogicException |
815
|
|
|
*/ |
816
|
14 |
|
public function append($content): Query { |
817
|
14 |
|
if (empty($this->_nodes) && |
818
|
10 |
|
$this->_useDocumentContext && |
819
|
10 |
|
!isset($this->getDocument()->documentElement)) { |
820
|
10 |
|
if ($callback = Constraints::filterCallable($content)) { |
821
|
1 |
|
$contentNode = $this->build()->getContentElement($callback(NULL, 0, '')); |
822
|
|
|
} else { |
823
|
9 |
|
$contentNode = $this->build()->getContentElement($content); |
824
|
|
|
} |
825
|
8 |
|
return $this->spawn($this->getDocument()->appendChild($contentNode)); |
826
|
|
|
} else { |
827
|
5 |
|
return $this->applyToSpawn( |
828
|
5 |
|
$this->_nodes, |
829
|
5 |
|
$content, |
830
|
|
|
function($targetNode, $contentNodes) { |
831
|
5 |
|
return $this->modify($targetNode)->appendChildren($contentNodes); |
832
|
5 |
|
} |
833
|
|
|
); |
834
|
|
|
} |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
/** |
838
|
|
|
* Append all of the matched elements to another, specified, set of elements. |
839
|
|
|
* Returns all of the inserted elements. |
840
|
|
|
* |
841
|
|
|
* @example appendTo.php Usage Example: FluentDOM\Query::appendTo() |
842
|
|
|
* @param string|array|\DOMNode|\DOMNodeList|Query $selector |
843
|
|
|
* @return Query |
844
|
|
|
*/ |
845
|
2 |
|
public function appendTo($selector): Query { |
846
|
2 |
|
return $this->applyToSelector( |
847
|
2 |
|
$selector, |
848
|
|
|
function($targetNode, $contentNodes) { |
849
|
2 |
|
return $this->modify($targetNode)->appendChildren($contentNodes); |
850
|
2 |
|
}, |
851
|
2 |
|
TRUE |
852
|
|
|
); |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
/** |
856
|
|
|
* Insert content before each of the matched elements. |
857
|
|
|
* |
858
|
|
|
* @example before.php Usage Example: FluentDOM\Query::before() |
859
|
|
|
* @param string|array|\DOMNode|\Traversable|callable $content |
860
|
|
|
* @return Query |
861
|
|
|
*/ |
862
|
2 |
|
public function before($content): Query { |
863
|
2 |
|
return $this->applyToSpawn( |
864
|
2 |
|
$this->_nodes, |
865
|
2 |
|
$content, |
866
|
|
|
function($targetNode, $contentNodes) { |
867
|
2 |
|
return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
868
|
2 |
|
} |
869
|
|
|
); |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
/** |
873
|
|
|
* Clone matched DOM Elements and select the clones. |
874
|
|
|
* |
875
|
|
|
* @example clone.php Usage Example: FluentDOM\Query:clone() |
876
|
|
|
* @return Query |
877
|
|
|
* @throws \OutOfBoundsException |
878
|
|
|
* @throws \InvalidArgumentException |
879
|
|
|
*/ |
880
|
|
|
public function clone(): Query { |
881
|
1 |
|
$result = $this->spawn(); |
882
|
1 |
|
foreach ($this->_nodes as $node) { |
883
|
|
|
/** @var \DOMNode $node */ |
884
|
1 |
|
$result->push($node->cloneNode(TRUE)); |
885
|
|
|
} |
886
|
1 |
|
return $result; |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
/** |
890
|
|
|
* Remove all child nodes from the set of matched elements. |
891
|
|
|
* |
892
|
|
|
* @example empty.php Usage Example: FluentDOM\Query:empty() |
893
|
|
|
* @return Query |
894
|
|
|
* @throws \InvalidArgumentException |
895
|
|
|
*/ |
896
|
|
|
public function empty(): Query { |
897
|
2 |
|
$this->each( |
898
|
|
|
function (\DOMNode $node) { |
899
|
2 |
|
$node->nodeValue = ''; |
900
|
2 |
|
} |
901
|
|
|
); |
902
|
2 |
|
$this->_useDocumentContext = TRUE; |
903
|
2 |
|
return $this; |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
/** |
907
|
|
|
* Insert all of the matched elements after another, specified, set of elements. |
908
|
|
|
* |
909
|
|
|
* @example insertAfter.php Usage Example: FluentDOM\Query::insertAfter() |
910
|
|
|
* @param string|array|\DOMNode|\Traversable $selector |
911
|
|
|
* @return Query |
912
|
|
|
* @throws \InvalidArgumentException |
913
|
|
|
*/ |
914
|
1 |
|
public function insertAfter($selector): Query { |
915
|
1 |
|
return $this->applyToSpawn( |
916
|
1 |
|
$this->build()->getTargetNodes($selector), |
917
|
1 |
|
$this->_nodes, |
918
|
|
|
function($targetNode, $contentNodes) { |
919
|
1 |
|
return $this->modify($targetNode)->insertNodesAfter($contentNodes); |
920
|
1 |
|
}, |
921
|
1 |
|
TRUE |
922
|
|
|
); |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
/** |
926
|
|
|
* Insert all of the matched elements before another, specified, set of elements. |
927
|
|
|
* |
928
|
|
|
* @example insertBefore.php Usage Example: FluentDOM\Query::insertBefore() |
929
|
|
|
* @param string|array|\DOMNode|\Traversable $selector |
930
|
|
|
* @return Query |
931
|
|
|
*/ |
932
|
1 |
|
public function insertBefore($selector): Query { |
933
|
1 |
|
return $this->applyToSelector( |
934
|
1 |
|
$selector, |
935
|
|
|
function($targetNode, $contentNodes) { |
936
|
1 |
|
return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
937
|
1 |
|
}, |
938
|
1 |
|
TRUE |
939
|
|
|
); |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
/** |
943
|
|
|
* Prepend content to the inside of every matched element. |
944
|
|
|
* |
945
|
|
|
* @example prepend.php Usage Example: FluentDOM\Query::prepend() |
946
|
|
|
* @param string|array|\DOMNode|\Traversable $content |
947
|
|
|
* @return Query |
948
|
|
|
*/ |
949
|
3 |
|
public function prepend($content): Query { |
950
|
3 |
|
return $this->applyToSpawn( |
951
|
3 |
|
$this->_nodes, |
952
|
3 |
|
$content, |
953
|
|
|
function($targetNode, $contentNodes) { |
954
|
3 |
|
return $this->modify($targetNode)->insertChildrenBefore($contentNodes); |
955
|
3 |
|
} |
956
|
|
|
); |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
/** |
960
|
|
|
* Prepend all of the matched elements to another, specified, set of elements. |
961
|
|
|
* Returns all of the inserted elements. |
962
|
|
|
* |
963
|
|
|
* @example prependTo.php Usage Example: FluentDOM\Query::prependTo() |
964
|
|
|
* @param string|array|\DOMNode|\DOMNodeList|Query $selector |
965
|
|
|
* @return Query list of all new elements |
966
|
|
|
*/ |
967
|
1 |
|
public function prependTo($selector): Query { |
968
|
1 |
|
return $this->applyToSelector( |
969
|
1 |
|
$selector, |
970
|
|
|
function($targetNode, $contentNodes) { |
971
|
1 |
|
return $this->modify($targetNode)->insertChildrenBefore($contentNodes); |
972
|
1 |
|
}, |
973
|
1 |
|
TRUE |
974
|
|
|
); |
975
|
|
|
} |
976
|
|
|
|
977
|
|
|
/** |
978
|
|
|
* Replaces the elements matched by the specified selector with the matched elements. |
979
|
|
|
* |
980
|
|
|
* @example replaceAll.php Usage Example: FluentDOM\Query::replaceAll() |
981
|
|
|
* @param string|array|\DOMNode|\Traversable $selector |
982
|
|
|
* @return Query |
983
|
|
|
* @throws \InvalidArgumentException |
984
|
|
|
*/ |
985
|
3 |
|
public function replaceAll($selector): Query { |
986
|
3 |
|
$result = $this->applyToSpawn( |
987
|
3 |
|
$targetNodes = $this->build()->getTargetNodes($selector), |
988
|
2 |
|
$this->_nodes, |
989
|
|
|
function($targetNode, $contentNodes) { |
990
|
2 |
|
return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
991
|
2 |
|
}, |
992
|
2 |
|
TRUE |
993
|
|
|
); |
994
|
2 |
|
$target = $this->spawn($targetNodes); |
995
|
2 |
|
$target->remove(); |
996
|
2 |
|
return $result; |
997
|
|
|
} |
998
|
|
|
|
999
|
|
|
/** |
1000
|
|
|
* Replaces all matched elements with the specified HTML or DOM elements. |
1001
|
|
|
* This returns the element that was just replaced, |
1002
|
|
|
* which has been removed from the DOM. |
1003
|
|
|
* |
1004
|
|
|
* @example replaceWith.php Usage Example: FluentDOM\Query::replaceWith() |
1005
|
|
|
* @param string|array|\DOMNode|\Traversable|callable $content |
1006
|
|
|
* @return Query |
1007
|
|
|
* @throws \InvalidArgumentException |
1008
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
1009
|
|
|
*/ |
1010
|
2 |
|
public function replaceWith($content): Query { |
1011
|
2 |
|
$this->apply( |
1012
|
2 |
|
$this->_nodes, |
1013
|
2 |
|
$content, |
1014
|
|
|
function($targetNode, $contentNodes) { |
1015
|
2 |
|
return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
1016
|
2 |
|
} |
1017
|
|
|
); |
1018
|
2 |
|
$this->remove(); |
1019
|
2 |
|
return $this; |
1020
|
|
|
} |
1021
|
|
|
|
1022
|
|
|
/** |
1023
|
|
|
* Removes all matched elements from the DOM. |
1024
|
|
|
* |
1025
|
|
|
* @example remove.php Usage Example: FluentDOM\Query::remove() |
1026
|
|
|
* @param string $selector selector |
1027
|
|
|
* @return Query removed elements |
1028
|
|
|
* @throws \OutOfBoundsException |
1029
|
|
|
* @throws \InvalidArgumentException |
1030
|
|
|
*/ |
1031
|
11 |
|
public function remove(string $selector = NULL): Query { |
1032
|
11 |
|
$result = $this->spawn(); |
1033
|
11 |
|
foreach ($this->_nodes as $node) { |
1034
|
11 |
|
if ($node->parentNode instanceof \DOMNode) { |
1035
|
11 |
|
if (NULL === $selector || $this->matches($selector, $node)) { |
1036
|
11 |
|
$result->push($node->parentNode->removeChild($node)); |
1037
|
|
|
} |
1038
|
|
|
} |
1039
|
|
|
} |
1040
|
11 |
|
return $result; |
1041
|
|
|
} |
1042
|
|
|
|
1043
|
|
|
/** |
1044
|
|
|
* Get the combined text contents of all matched elements or |
1045
|
|
|
* set the text contents of all matched elements. |
1046
|
|
|
* |
1047
|
|
|
* @example text.php Usage Example: FluentDOM\Query::text() |
1048
|
|
|
* @param string|callable $text |
1049
|
|
|
* @return string|Query |
1050
|
|
|
* @throws \InvalidArgumentException |
1051
|
|
|
*/ |
1052
|
3 |
|
public function text($text = NULL) { |
1053
|
3 |
|
if (NULL !== $text) { |
1054
|
2 |
|
$callback = Constraints::filterCallable($text); |
1055
|
2 |
|
foreach ($this->_nodes as $index => $node) { |
1056
|
2 |
|
$node->nodeValue = $callback ? $callback($node, $index, $node->nodeValue): $text; |
1057
|
|
|
} |
1058
|
2 |
|
return $this; |
1059
|
|
|
} |
1060
|
1 |
|
$result = ''; |
1061
|
1 |
|
foreach ($this->_nodes as $node) { |
1062
|
1 |
|
$result .= $node->textContent; |
1063
|
|
|
} |
1064
|
1 |
|
return $result; |
1065
|
|
|
} |
1066
|
|
|
|
1067
|
|
|
/** |
1068
|
|
|
* Wrap each matched element with the specified content. |
1069
|
|
|
* |
1070
|
|
|
* If $content contains several elements the first one is used |
1071
|
|
|
* |
1072
|
|
|
* @example wrap.php Usage Example: FluentDOM\Query::wrap() |
1073
|
|
|
* @param string|array|\DOMNode|\Traversable|callable $content |
1074
|
|
|
* @return Query |
1075
|
|
|
*/ |
1076
|
6 |
|
public function wrap($content): Query { |
1077
|
6 |
|
return $this->spawn($this->wrapNodes($this->_nodes, $content)); |
1078
|
|
|
} |
1079
|
|
|
|
1080
|
|
|
/** |
1081
|
|
|
* Wrap al matched elements with the specified content |
1082
|
|
|
* |
1083
|
|
|
* If the matched elements are not siblings, wrap each group of siblings. |
1084
|
|
|
* |
1085
|
|
|
* @example wrapAll.php Usage Example: FluentDOM::wrapAll() |
1086
|
|
|
* @param string|array|\DOMNode|\Traversable $content |
1087
|
|
|
* @return Query |
1088
|
|
|
* @throws \FluentDOM\Exceptions\LoadingError\EmptyResult |
1089
|
|
|
* @throws \OutOfBoundsException |
1090
|
|
|
* @throws \InvalidArgumentException |
1091
|
|
|
*/ |
1092
|
2 |
|
public function wrapAll($content): Query { |
1093
|
2 |
|
$result = $this->spawn(); |
1094
|
2 |
|
if ($groups = $this->getGroupedNodes()) { |
1095
|
2 |
|
$result->push( |
1096
|
2 |
|
$this->wrapGroupedNodes( |
1097
|
2 |
|
$groups, $this->build()->getContentElement($content) |
1098
|
|
|
) |
1099
|
|
|
); |
1100
|
|
|
} |
1101
|
2 |
|
return $result; |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
|
|
/** |
1105
|
|
|
* group selected elements by previous node - ignore whitespace text nodes |
1106
|
|
|
* |
1107
|
|
|
* @return array|bool |
1108
|
|
|
*/ |
1109
|
2 |
|
private function getGroupedNodes() { |
1110
|
2 |
|
$current = NULL; |
1111
|
2 |
|
$counter = 0; |
1112
|
2 |
|
$groups = []; |
1113
|
2 |
|
foreach ($this->_nodes as $node) { |
1114
|
2 |
|
$previous = $node->previousSibling; |
1115
|
2 |
|
while ($previous instanceof \DOMText && $previous->isWhitespaceInElementContent()) { |
1116
|
2 |
|
$previous = $previous->previousSibling; |
1117
|
|
|
} |
1118
|
2 |
|
if ($previous !== $current) { |
1119
|
2 |
|
$counter++; |
1120
|
|
|
} |
1121
|
2 |
|
$groups[$counter][] = $node; |
1122
|
2 |
|
$current = $node; |
1123
|
|
|
} |
1124
|
2 |
|
return count($groups) > 0 ? $groups : FALSE; |
1125
|
|
|
} |
1126
|
|
|
|
1127
|
|
|
/** |
1128
|
|
|
* Wrap grouped nodes |
1129
|
|
|
* |
1130
|
|
|
* @param array $groups |
1131
|
|
|
* @param \DOMElement $template |
1132
|
|
|
* @return array |
1133
|
|
|
*/ |
1134
|
2 |
|
private function wrapGroupedNodes(array $groups, \DOMElement $template): array { |
1135
|
2 |
|
$result = []; |
1136
|
2 |
|
$simple = FALSE; |
1137
|
2 |
|
foreach ($groups as $group) { |
1138
|
2 |
|
if (isset($group[0])) { |
1139
|
2 |
|
$node = $group[0]; |
1140
|
|
|
/** |
1141
|
|
|
* @var \DOMElement $target |
1142
|
|
|
* @var \DOMElement $wrapper |
1143
|
|
|
*/ |
1144
|
2 |
|
list($target, $wrapper) = $this->build()->getWrapperNodes( |
1145
|
2 |
|
$template, |
1146
|
2 |
|
$simple |
1147
|
|
|
); |
1148
|
2 |
|
if ($node->parentNode instanceof \DOMNode) { |
1149
|
2 |
|
$node->parentNode->insertBefore($wrapper, $node); |
1150
|
|
|
} |
1151
|
2 |
|
foreach ($group as $node) { |
1152
|
2 |
|
$target->appendChild($node); |
1153
|
|
|
} |
1154
|
2 |
|
$result[] = $node; |
1155
|
|
|
} |
1156
|
|
|
} |
1157
|
2 |
|
return $result; |
1158
|
|
|
} |
1159
|
|
|
|
1160
|
|
|
/** |
1161
|
|
|
* Wrap the inner child contents of each matched element |
1162
|
|
|
* (including text nodes) with an XML structure. |
1163
|
|
|
* |
1164
|
|
|
* @example wrapInner.php Usage Example: FluentDOM\Query::wrapInner() |
1165
|
|
|
* @param string|array|\DOMNode|\Traversable $content |
1166
|
|
|
* @return Query |
1167
|
|
|
*/ |
1168
|
2 |
|
public function wrapInner($content): Query { |
1169
|
2 |
|
$elements = []; |
1170
|
2 |
|
foreach ($this->_nodes as $node) { |
1171
|
2 |
|
foreach ($node->childNodes as $childNode) { |
1172
|
2 |
|
if (Constraints::filterNode($childNode)) { |
1173
|
2 |
|
$elements[] = $childNode; |
1174
|
|
|
} |
1175
|
|
|
} |
1176
|
|
|
} |
1177
|
2 |
|
return $this->spawn($this->wrapNodes($elements, $content)); |
1178
|
|
|
} |
1179
|
|
|
|
1180
|
|
|
/** |
1181
|
|
|
* Get xml contents of the first matched element or set the |
1182
|
|
|
* xml contents of all selected element nodes. |
1183
|
|
|
* |
1184
|
|
|
* @example xml.php Usage Example: FluentDOM::xml() |
1185
|
|
|
* @param string|callable|NULL $xml XML fragment |
1186
|
|
|
* @return string|$this |
1187
|
|
|
* @throws \InvalidArgumentException |
1188
|
|
|
* @throws \FluentDOM\Exceptions\InvalidFragmentLoader |
1189
|
|
|
* @throws \UnexpectedValueException |
1190
|
|
|
* @throws \LogicException |
1191
|
|
|
*/ |
1192
|
7 |
|
public function xml($xml = NULL) { |
1193
|
7 |
|
return $this->content( |
1194
|
7 |
|
$xml, |
1195
|
|
|
function($node) { |
1196
|
3 |
|
return $this->build()->getInnerXml($node); |
1197
|
7 |
|
}, |
1198
|
|
|
function($node) { |
1199
|
4 |
|
return $this->build()->getFragment($node); |
1200
|
7 |
|
}, |
1201
|
|
|
function($node, $fragment) { |
1202
|
3 |
|
$this->modify($node)->replaceChildren($fragment); |
1203
|
7 |
|
} |
1204
|
|
|
); |
1205
|
|
|
} |
1206
|
|
|
|
1207
|
|
|
/** |
1208
|
|
|
* Get the first matched node as XML or replace each |
1209
|
|
|
* matched nodes with the provided fragment. |
1210
|
|
|
* |
1211
|
|
|
* @param string|callable|NULL $xml |
1212
|
|
|
* @return string|$this |
1213
|
|
|
* @throws \LogicException |
1214
|
|
|
* @throws \UnexpectedValueException |
1215
|
|
|
* @throws \InvalidArgumentException |
1216
|
|
|
* @throws \FluentDOM\Exceptions\InvalidFragmentLoader |
1217
|
|
|
*/ |
1218
|
7 |
View Code Duplication |
public function outerXml($xml = NULL) { |
1219
|
7 |
|
return $this->outerContent( |
1220
|
7 |
|
$xml, |
1221
|
|
|
function($node) { |
1222
|
3 |
|
return $this->getDocument()->saveXML($node); |
1223
|
7 |
|
}, |
1224
|
|
|
function($xml) { |
1225
|
4 |
|
return $this->build()->getFragment($xml); |
1226
|
7 |
|
} |
1227
|
|
|
); |
1228
|
|
|
} |
1229
|
|
|
|
1230
|
|
|
/** |
1231
|
|
|
* Get the first matched node as HTML or replace each |
1232
|
|
|
* matched nodes with the provided fragment. |
1233
|
|
|
* |
1234
|
|
|
* @param string|callable|NULL $html |
1235
|
|
|
* @return string|$this |
1236
|
|
|
* @throws \LogicException |
1237
|
|
|
* @throws \UnexpectedValueException |
1238
|
|
|
* @throws \InvalidArgumentException |
1239
|
|
|
* @throws \FluentDOM\Exceptions\InvalidFragmentLoader |
1240
|
|
|
*/ |
1241
|
7 |
View Code Duplication |
public function outerHtml($html = NULL) { |
1242
|
7 |
|
return $this->outerContent( |
1243
|
7 |
|
$html, |
1244
|
|
|
function($node) { |
1245
|
3 |
|
return $this->getDocument()->saveHTML($node); |
1246
|
7 |
|
}, |
1247
|
|
|
function($html) { |
1248
|
4 |
|
return $this->build()->getFragment($html, 'text/html'); |
1249
|
7 |
|
} |
1250
|
|
|
); |
1251
|
|
|
} |
1252
|
|
|
|
1253
|
|
|
/** |
1254
|
|
|
* Get html contents of the first matched element or set the |
1255
|
|
|
* html contents of all selected element nodes. |
1256
|
|
|
* |
1257
|
|
|
* @param string|callable|NULL $html |
1258
|
|
|
* @return string|$this |
1259
|
|
|
* @throws \LogicException |
1260
|
|
|
* @throws \UnexpectedValueException |
1261
|
|
|
* @throws \InvalidArgumentException |
1262
|
|
|
* @throws \FluentDOM\Exceptions\InvalidFragmentLoader |
1263
|
|
|
*/ |
1264
|
4 |
|
public function html($html = NULL) { |
1265
|
4 |
|
return $this->content( |
1266
|
4 |
|
$html, |
1267
|
|
|
function(\DOMNode $parent) { |
1268
|
2 |
|
$result = ''; |
1269
|
2 |
|
foreach ($parent->childNodes as $node) { |
1270
|
2 |
|
$result .= $this->getDocument()->saveHTML($node); |
1271
|
|
|
} |
1272
|
2 |
|
return $result; |
1273
|
4 |
|
}, |
1274
|
|
|
function($html) { |
1275
|
2 |
|
return $this->build()->getFragment($html, 'text/html'); |
1276
|
4 |
|
}, |
1277
|
|
|
function($node, $fragment) { |
1278
|
2 |
|
$this->modify($node)->replaceChildren($fragment); |
1279
|
4 |
|
} |
1280
|
|
|
); |
1281
|
|
|
} |
1282
|
|
|
|
1283
|
|
|
/** |
1284
|
|
|
* @param string|callable|NULL $content |
1285
|
|
|
* @param callable $export |
1286
|
|
|
* @param callable $import |
1287
|
|
|
* @param callable $insert |
1288
|
|
|
* @return string|$this |
1289
|
|
|
* @throws \InvalidArgumentException |
1290
|
|
|
*/ |
1291
|
25 |
|
private function content($content, callable $export, callable $import, callable $insert) { |
1292
|
25 |
|
if (NULL !== $content) { |
1293
|
14 |
|
$callback = Constraints::filterCallable($content); |
1294
|
14 |
|
if ($callback) { |
1295
|
4 |
|
foreach ($this->_nodes as $index => $node) { |
1296
|
4 |
|
$contentString = $callback($node, $index, $export($node)); |
1297
|
4 |
|
$insert($node, $import($contentString)); |
1298
|
|
|
} |
1299
|
|
|
} else { |
1300
|
10 |
|
$fragment = $import($content); |
1301
|
7 |
|
foreach ($this->_nodes as $node) { |
1302
|
7 |
|
$insert($node, $fragment); |
1303
|
|
|
} |
1304
|
|
|
} |
1305
|
11 |
|
return $this; |
1306
|
|
|
} |
1307
|
11 |
|
if (isset($this->_nodes[0])) { |
1308
|
7 |
|
return $export($this->_nodes[0]); |
1309
|
|
|
} |
1310
|
4 |
|
return ''; |
1311
|
|
|
} |
1312
|
|
|
|
1313
|
|
|
/** |
1314
|
|
|
* @param string|callable|NULL $content |
1315
|
|
|
* @param callable $export |
1316
|
|
|
* @param callable $import |
1317
|
|
|
* @return $this|string |
1318
|
|
|
* @throws \InvalidArgumentException |
1319
|
|
|
*/ |
1320
|
14 |
|
private function outerContent($content, callable $export, callable $import) { |
1321
|
14 |
|
return $this->content( |
1322
|
14 |
|
$content, |
1323
|
14 |
|
$export, |
1324
|
14 |
|
$import, |
1325
|
|
|
function(\DOMNode $node, $fragment) { |
1326
|
6 |
|
$this->modify($node)->replaceNode($fragment); |
1327
|
14 |
|
} |
1328
|
|
|
); |
1329
|
|
|
} |
1330
|
|
|
|
1331
|
|
|
/**************************** |
1332
|
|
|
* Manipulation - Attributes |
1333
|
|
|
***************************/ |
1334
|
|
|
|
1335
|
|
|
/** |
1336
|
|
|
* @param string|array|NULL $names |
1337
|
|
|
* @throws \InvalidArgumentException |
1338
|
|
|
* @return array|NULL |
1339
|
|
|
*/ |
1340
|
8 |
|
private function getNamesList($names) { |
1341
|
8 |
|
if (is_array($names)) { |
1342
|
2 |
|
return $names; |
1343
|
|
|
} |
1344
|
6 |
|
if (is_string($names) && $names !== '*' && $names !== '') { |
1345
|
2 |
|
return [$names]; |
1346
|
|
|
} |
1347
|
4 |
|
if (NULL !== $names && $names !== '*') { |
1348
|
2 |
|
throw new \InvalidArgumentException('Invalid names argument provided.'); |
1349
|
|
|
} |
1350
|
2 |
|
return NULL; |
1351
|
|
|
} |
1352
|
|
|
|
1353
|
|
|
/** |
1354
|
|
|
* @param string|array|\Traversable $name |
1355
|
|
|
* @param string|float|int|NULL|callable $value |
1356
|
|
|
* @return array|\Traversable |
1357
|
|
|
* @throws \InvalidArgumentException |
1358
|
|
|
*/ |
1359
|
20 |
|
private function getSetterValues($name, $value) { |
1360
|
20 |
|
if (is_string($name)) { |
1361
|
15 |
|
return [(string)$name => $value]; |
1362
|
|
|
} |
1363
|
5 |
|
if (is_array($name) || $name instanceOf \Traversable) { |
1364
|
4 |
|
return $name; |
1365
|
|
|
} |
1366
|
1 |
|
throw new \InvalidArgumentException('Invalid css property name argument type.'); |
1367
|
|
|
} |
1368
|
|
|
|
1369
|
|
|
/** |
1370
|
|
|
* Access a property on the first matched element or set the attribute(s) of all matched elements |
1371
|
|
|
* |
1372
|
|
|
* @example attr.php Usage Example: FluentDOM\Query::attr() Read an attribute value. |
1373
|
|
|
* @param string|array $attribute attribute name or attribute list |
1374
|
|
|
* @param array ...$arguments |
1375
|
|
|
* @return $this|string attribute value or $this |
1376
|
|
|
* @throws \InvalidArgumentException |
1377
|
|
|
*/ |
1378
|
18 |
|
public function attr($attribute, ...$arguments) { |
1379
|
18 |
|
if (is_string($attribute) && count($arguments) === 0) { |
1380
|
|
|
//empty value - read attribute from first element in list |
1381
|
10 |
|
$attribute = (new QualifiedName($attribute))->name; |
1382
|
9 |
|
$node = $this->getFirstElement(); |
1383
|
9 |
|
if ($node && $node->hasAttribute($attribute)) { |
1384
|
6 |
|
return $node->getAttribute($attribute); |
1385
|
|
|
} |
1386
|
3 |
|
return NULL; |
1387
|
|
|
} else { |
1388
|
12 |
|
$attributes = $this->getSetterValues($attribute, $arguments[0] ?? NULL); |
1389
|
|
|
// set attributes on each element |
1390
|
12 |
|
foreach ($attributes as $key => $value) { |
1391
|
12 |
|
$name = (new QualifiedName($key))->name; |
1392
|
6 |
|
$callback = Constraints::filterCallable($value); |
1393
|
6 |
|
$this->each( |
1394
|
|
|
function(\DOMElement $node, $index) use ($name, $value, $callback) { |
1395
|
6 |
|
$node->setAttribute( |
1396
|
6 |
|
$name, |
1397
|
6 |
|
$callback |
1398
|
1 |
|
? (string)$callback($node, $index, $node->getAttribute($name)) |
1399
|
6 |
|
: (string)$value |
1400
|
|
|
); |
1401
|
6 |
|
}, |
1402
|
6 |
|
TRUE |
1403
|
|
|
); |
1404
|
|
|
} |
1405
|
|
|
} |
1406
|
6 |
|
return $this; |
1407
|
|
|
} |
1408
|
|
|
|
1409
|
|
|
/** |
1410
|
|
|
* Returns TRUE if the specified attribute is present on at least one of |
1411
|
|
|
* the set of matched elements. |
1412
|
|
|
* |
1413
|
|
|
* @param string $name |
1414
|
|
|
* @return bool |
1415
|
|
|
*/ |
1416
|
3 |
|
public function hasAttr($name) { |
1417
|
3 |
|
foreach ($this->_nodes as $node) { |
1418
|
3 |
|
if ($node instanceof \DOMElement && $node->hasAttribute($name)) { |
1419
|
2 |
|
return TRUE; |
1420
|
|
|
} |
1421
|
|
|
} |
1422
|
1 |
|
return FALSE; |
1423
|
|
|
} |
1424
|
|
|
|
1425
|
|
|
/** |
1426
|
|
|
* Remove an attribute from each of the matched elements. If $name is NULL or *, |
1427
|
|
|
* all attributes will be deleted. |
1428
|
|
|
* |
1429
|
|
|
* @example removeAttr.php Usage Example: FluentDOM\Query::removeAttr() |
1430
|
|
|
* @param string|array $name |
1431
|
|
|
* @throws \InvalidArgumentException |
1432
|
|
|
* @return $this|Query |
1433
|
|
|
*/ |
1434
|
4 |
|
public function removeAttr($name): Query { |
1435
|
4 |
|
$names = $this->getNamesList($name); |
1436
|
3 |
|
$this->each( |
1437
|
|
|
function(\DOMElement $node) use ($names) { |
1438
|
|
|
/** @noinspection PhpParamsInspection */ |
1439
|
3 |
|
$attributes = $names ?? array_keys(iterator_to_array($node->attributes)); |
1440
|
3 |
|
foreach ($attributes as $attribute) { |
1441
|
3 |
|
if ($node->hasAttribute($attribute)) { |
1442
|
3 |
|
$node->removeAttribute($attribute); |
1443
|
|
|
} |
1444
|
|
|
} |
1445
|
3 |
|
}, |
1446
|
3 |
|
TRUE |
1447
|
|
|
); |
1448
|
3 |
|
return $this; |
1449
|
|
|
} |
1450
|
|
|
|
1451
|
|
|
/************************* |
1452
|
|
|
* Manipulation - Classes |
1453
|
|
|
************************/ |
1454
|
|
|
|
1455
|
|
|
/** |
1456
|
|
|
* Adds the specified class(es) to each of the set of matched elements. |
1457
|
|
|
* |
1458
|
|
|
* @param string|callable $class |
1459
|
|
|
* @return Query |
1460
|
|
|
*/ |
1461
|
1 |
|
public function addClass($class) { |
1462
|
1 |
|
return $this->toggleClass($class, TRUE); |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
|
|
/** |
1466
|
|
|
* Returns TRUE if the specified class is present on at least one of the set of matched elements. |
1467
|
|
|
* |
1468
|
|
|
* @param string $class |
1469
|
|
|
* @return bool |
1470
|
|
|
*/ |
1471
|
2 |
|
public function hasClass(string $class): bool { |
1472
|
2 |
|
foreach ($this->_nodes as $node) { |
1473
|
2 |
|
if ($node instanceof \DOMElement && $node->hasAttribute('class')) { |
1474
|
2 |
|
$classes = preg_split('(\s+)', trim($node->getAttribute('class'))); |
1475
|
2 |
|
if (in_array($class, $classes, TRUE)) { |
1476
|
1 |
|
return TRUE; |
1477
|
|
|
} |
1478
|
|
|
} |
1479
|
|
|
} |
1480
|
1 |
|
return FALSE; |
1481
|
|
|
} |
1482
|
|
|
|
1483
|
|
|
/** |
1484
|
|
|
* Removes all or the specified class(es) from the set of matched elements. |
1485
|
|
|
* |
1486
|
|
|
* @param string|callable $class |
1487
|
|
|
* @return $this|Query |
1488
|
|
|
* @throws \InvalidArgumentException |
1489
|
|
|
*/ |
1490
|
2 |
|
public function removeClass($class = ''): Query { |
1491
|
2 |
|
return $this->toggleClass($class, FALSE); |
1492
|
|
|
} |
1493
|
|
|
|
1494
|
|
|
/** |
1495
|
|
|
* Adds the specified classes if the switch is TRUE, |
1496
|
|
|
* removes the specified classes if the switch is FALSE, |
1497
|
|
|
* toggles the specified classes if the switch is NULL. |
1498
|
|
|
* |
1499
|
|
|
* @example toggleClass.php Usage Example: FluentDOM\Query::toggleClass() |
1500
|
|
|
* @param string|callable $class |
1501
|
|
|
* @param NULL|bool $switch toggle if NULL, add if TRUE, remove if FALSE |
1502
|
|
|
* @return $this|Query |
1503
|
|
|
* @throws \InvalidArgumentException |
1504
|
|
|
*/ |
1505
|
6 |
|
public function toggleClass($class, bool $switch = NULL): Query { |
1506
|
6 |
|
$callback = Constraints::filterCallable($class); |
1507
|
6 |
|
$this->each( |
1508
|
|
|
function(\DOMElement $node, $index) use ($class, $switch, $callback) { |
1509
|
6 |
|
$classString = $callback ? $callback($node, $index, $node->getAttribute('class')) : $class; |
1510
|
6 |
|
if (empty($classString) && !(bool)$switch) { |
1511
|
1 |
|
if ($node->hasAttribute('class')) { |
1512
|
1 |
|
$node->removeAttribute('class'); |
1513
|
|
|
} |
1514
|
|
|
} else { |
1515
|
5 |
|
$modified = $this->changeClassString( |
1516
|
5 |
|
$node->getAttribute('class'), |
1517
|
5 |
|
$classString, |
1518
|
5 |
|
$switch |
1519
|
|
|
); |
1520
|
5 |
|
if (FALSE !== $modified) { |
1521
|
5 |
|
if (empty($modified)) { |
1522
|
1 |
|
$node->removeAttribute('class'); |
1523
|
|
|
} else { |
1524
|
5 |
|
$node->setAttribute('class', $modified); |
1525
|
|
|
} |
1526
|
|
|
} |
1527
|
|
|
} |
1528
|
6 |
|
}, |
1529
|
6 |
|
TRUE |
1530
|
|
|
); |
1531
|
6 |
|
return $this; |
1532
|
|
|
} |
1533
|
|
|
|
1534
|
|
|
/** |
1535
|
|
|
* Change a class string |
1536
|
|
|
* |
1537
|
|
|
* Adds the specified classes if the switch is TRUE, |
1538
|
|
|
* removes the specified classes if the switch is FALSE, |
1539
|
|
|
* toggles the specified classes if the switch is NULL. |
1540
|
|
|
* |
1541
|
|
|
* @param string $current |
1542
|
|
|
* @param string $toggle |
1543
|
|
|
* @param bool|NULL $switch |
1544
|
|
|
* @return FALSE|string |
1545
|
|
|
*/ |
1546
|
5 |
|
private function changeClassString(string $current, string $toggle, bool $switch = NULL) { |
1547
|
5 |
|
$currentClasses = array_flip( |
1548
|
5 |
|
preg_split('(\s+)', trim($current), 0, PREG_SPLIT_NO_EMPTY) |
1549
|
|
|
); |
1550
|
5 |
|
$toggleClasses = array_unique( |
1551
|
5 |
|
preg_split('(\s+)', trim($toggle), 0, PREG_SPLIT_NO_EMPTY) |
1552
|
|
|
); |
1553
|
5 |
|
$modified = FALSE; |
1554
|
5 |
|
foreach ($toggleClasses as $class) { |
1555
|
|
|
if ( |
1556
|
5 |
|
isset($currentClasses[$class]) && |
1557
|
4 |
|
(NULL === $switch || FALSE === $switch) |
1558
|
|
|
) { |
1559
|
4 |
|
unset($currentClasses[$class]); |
1560
|
4 |
|
$modified = TRUE; |
1561
|
5 |
|
} elseif (NULL === $switch || TRUE === $switch) { |
1562
|
4 |
|
$currentClasses[$class] = TRUE; |
1563
|
4 |
|
$modified = TRUE; |
1564
|
|
|
} |
1565
|
|
|
} |
1566
|
5 |
|
return $modified |
1567
|
5 |
|
? implode(' ', array_keys($currentClasses)) |
1568
|
5 |
|
: FALSE; |
1569
|
|
|
} |
1570
|
|
|
|
1571
|
|
|
/************************************* |
1572
|
|
|
* Manipulation - CSS Style Attribute |
1573
|
|
|
************************************/ |
1574
|
|
|
|
1575
|
|
|
/** |
1576
|
|
|
* get or set CSS values in style attributes |
1577
|
|
|
* |
1578
|
|
|
* @param string|array $property |
1579
|
|
|
* @param array ...$arguments |
1580
|
|
|
* @throws \InvalidArgumentException |
1581
|
|
|
* @return string|NULL|$this |
1582
|
|
|
*/ |
1583
|
16 |
|
public function css($property, ...$arguments) { |
1584
|
16 |
|
if (is_string($property) && count($arguments) === 0) { |
1585
|
4 |
|
$properties = new Query\Css\Properties((string)$this->attr('style')); |
1586
|
4 |
|
if (isset($properties[$property])) { |
1587
|
2 |
|
return $properties[$property]; |
1588
|
|
|
} |
1589
|
2 |
|
return NULL; |
1590
|
|
|
} |
1591
|
12 |
|
$values = $this->getSetterValues($property, $arguments[0] ?? NULL); |
1592
|
|
|
//set list of properties to all elements |
1593
|
11 |
|
$this->each( |
1594
|
|
|
function(\DOMElement $node, $index) use ($values) { |
1595
|
11 |
|
$properties = new Query\Css\Properties($node->getAttribute('style')); |
1596
|
11 |
|
foreach ($values as $name => $value) { |
1597
|
11 |
|
$properties[$name] = $properties->compileValue( |
1598
|
11 |
|
$value, $node, $index, $properties[$name] ?? NULL |
1599
|
|
|
); |
1600
|
|
|
} |
1601
|
9 |
|
if (count($properties) > 0) { |
1602
|
6 |
|
$node->setAttribute('style', (string)$properties); |
1603
|
3 |
|
} elseif ($node->hasAttribute('style')) { |
1604
|
3 |
|
$node->removeAttribute('style'); |
1605
|
|
|
} |
1606
|
11 |
|
}, |
1607
|
11 |
|
TRUE |
1608
|
|
|
); |
1609
|
9 |
|
return $this; |
1610
|
|
|
} |
1611
|
|
|
|
1612
|
|
|
/********************************* |
1613
|
|
|
* Manipulation - Data Attributes |
1614
|
|
|
********************************/ |
1615
|
|
|
|
1616
|
|
|
/** |
1617
|
|
|
* Read a data attribute from the first node or set data attributes on all selected nodes. |
1618
|
|
|
* |
1619
|
|
|
* @example data.php Usage Example: FluentDOM\Query::data() |
1620
|
|
|
* @param string|array $name data attribute identifier or array of data attributes to set |
1621
|
|
|
* @param [mixed] ...$arguments |
1622
|
|
|
* @return mixed |
1623
|
|
|
* @throws \InvalidArgumentException |
1624
|
|
|
*/ |
1625
|
5 |
|
public function data($name, ...$arguments) { |
1626
|
5 |
|
if (is_string($name) && count($arguments) === 0) { |
1627
|
2 |
|
if ($node = $this->getFirstElement()) { |
1628
|
1 |
|
return (new Query\Data($node))->$name; |
1629
|
|
|
} |
1630
|
1 |
|
return NULL; |
1631
|
|
|
} |
1632
|
3 |
|
$values = $this->getSetterValues($name, $arguments[0] ?? NULL); |
1633
|
3 |
|
$this->each( |
1634
|
|
|
function(\DOMElement $node) use ($values) { |
1635
|
3 |
|
$data = new Query\Data($node); |
1636
|
3 |
|
foreach ($values as $dataName => $dataValue) { |
1637
|
3 |
|
$data->$dataName = $dataValue; |
1638
|
|
|
} |
1639
|
3 |
|
}, |
1640
|
3 |
|
TRUE |
1641
|
|
|
); |
1642
|
3 |
|
return $this; |
1643
|
|
|
} |
1644
|
|
|
|
1645
|
|
|
/** |
1646
|
|
|
* Remove an data - attribute from each of the matched elements. If $name is NULL or *, |
1647
|
|
|
* all data attributes will be deleted. |
1648
|
|
|
* |
1649
|
|
|
* @example removeData.php Usage Example: FluentDOM\Query::removeData() |
1650
|
|
|
* @param string|array|NULL $name |
1651
|
|
|
* @throws \InvalidArgumentException |
1652
|
|
|
* @return Query |
1653
|
|
|
*/ |
1654
|
4 |
|
public function removeData($name = NULL): Query { |
1655
|
4 |
|
$names = $this->getNamesList($name); |
1656
|
3 |
|
$this->each( |
1657
|
3 |
|
function ($node) use ($names) { |
1658
|
3 |
|
$data = new Query\Data($node); |
1659
|
3 |
|
if (is_array($names)) { |
1660
|
2 |
|
foreach ($names as $dataName) { |
1661
|
2 |
|
unset($data->$dataName); |
1662
|
|
|
} |
1663
|
|
|
} else { |
1664
|
1 |
|
foreach ($data as $dataName => $dataValue) { |
1665
|
1 |
|
unset($data->$dataName); |
1666
|
|
|
} |
1667
|
|
|
} |
1668
|
3 |
|
}, |
1669
|
3 |
|
TRUE |
1670
|
|
|
); |
1671
|
3 |
|
return $this; |
1672
|
|
|
} |
1673
|
|
|
|
1674
|
|
|
/** |
1675
|
|
|
* Validate if the element has an data attributes attached. If it is called without an |
1676
|
|
|
* actual $element parameter, it will check the first matched node. |
1677
|
|
|
* |
1678
|
|
|
* @param \DOMElement|NULL $element |
1679
|
|
|
* @return bool |
1680
|
|
|
*/ |
1681
|
5 |
|
public function hasData(\DOMElement $element = NULL): bool { |
1682
|
5 |
|
if ($element || ($element = $this->getFirstElement())) { |
1683
|
4 |
|
$data = new Query\Data($element); |
1684
|
4 |
|
return count($data) > 0; |
1685
|
|
|
} |
1686
|
1 |
|
return FALSE; |
1687
|
|
|
} |
1688
|
|
|
} |
1689
|
|
|
} |