@asamuzakjp/dom-selector 0.11.1 → 0.12.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.
package/package.json CHANGED
@@ -42,5 +42,5 @@
42
42
  "test": "c8 --reporter=text mocha --exit test/**/*.test.js",
43
43
  "tsc": "npx tsc"
44
44
  },
45
- "version": "0.11.1"
45
+ "version": "0.12.0"
46
46
  }
package/src/js/matcher.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  ATTRIBUTE_SELECTOR, CLASS_SELECTOR, COMBINATOR, IDENTIFIER, ID_SELECTOR,
14
14
  NTH, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, TYPE_SELECTOR
15
15
  } = require('./constant.js');
16
+ const DOCUMENT_POSITION_CONTAINS = 8;
16
17
  const ELEMENT_NODE = 1;
17
18
  const FILTER_ACCEPT = 1;
18
19
  const FILTER_REJECT = 2;
@@ -29,6 +30,36 @@ const PSEUDO_NTH = /^nth-(?:last-)?(?:child|of-type)$/;
29
30
  const REPLACE_CHAR = /[\0\uD800-\uDFFF]/g;
30
31
  const WHITESPACE = /^[\n\r\f]/;
31
32
 
33
+ /**
34
+ * is content editable
35
+ * NOTE: not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
36
+ * @param {object} node - Element
37
+ * @returns {boolean} - result
38
+ */
39
+ const isContentEditable = (node = {}) => {
40
+ let bool;
41
+ if (node.nodeType === ELEMENT_NODE) {
42
+ if (node.ownerDocument.designMode === 'on') {
43
+ bool = true;
44
+ } else if (node.hasAttribute('contenteditable')) {
45
+ const attr = node.getAttribute('contenteditable');
46
+ if (/^(?:plaintext-only|true)$/.test(attr) || attr === '') {
47
+ bool = true;
48
+ } else if (attr === 'inherit') {
49
+ let parent = node.parentNode;
50
+ while (parent) {
51
+ if (isContentEditable(parent)) {
52
+ bool = true;
53
+ break;
54
+ }
55
+ parent = parent.parentNode;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return !!bool;
61
+ };
62
+
32
63
  /**
33
64
  * unescape selector
34
65
  * @param {string} selector - CSS selector
@@ -88,9 +119,9 @@ const collectNthChild = (anb = {}, node = {}) => {
88
119
  const l = arr.length;
89
120
  const items = [];
90
121
  if (selector) {
91
- const a = new Matcher(selector, ownerDocument).querySelectorAll();
92
- if (a.length) {
93
- items.push(...a);
122
+ const ar = new Matcher(selector, ownerDocument).querySelectorAll();
123
+ if (ar.length) {
124
+ items.push(...ar);
94
125
  }
95
126
  }
96
127
  // :first-child, :last-child, :nth-child(0 of S)
@@ -664,14 +695,12 @@ const matchPseudoClassSelector = (
664
695
  switch (astName) {
665
696
  case 'any-link':
666
697
  case 'link':
667
- // TBD: what about namespaced href? e.g. xlink:href
668
- if (node.hasAttribute('href')) {
698
+ if (/^a(?:rea)?$/.test(localName) && node.hasAttribute('href')) {
669
699
  matched.push(node);
670
700
  }
671
701
  break;
672
702
  case 'local-link':
673
- // TBD: what about namespaced href? e.g. xlink:href
674
- if (node.hasAttribute('href')) {
703
+ if (/^a(?:rea)?$/.test(localName) && node.hasAttribute('href')) {
675
704
  const attrURL = new URL(node.getAttribute('href'), docURL.href);
676
705
  if (attrURL.origin === docURL.origin &&
677
706
  attrURL.pathname === docURL.pathname) {
@@ -736,11 +765,24 @@ const matchPseudoClassSelector = (
736
765
  }
737
766
  break;
738
767
  case 'disabled':
739
- if ((HTML_FORM_INPUT.test(localName) ||
740
- HTML_FORM_PARTS.test(localName) ||
741
- isCustomElementName(localName)) &&
742
- node.hasAttribute('disabled')) {
743
- matched.push(node);
768
+ if (HTML_FORM_INPUT.test(localName) ||
769
+ HTML_FORM_PARTS.test(localName) ||
770
+ isCustomElementName(localName)) {
771
+ if (node.hasAttribute('disabled')) {
772
+ matched.push(node);
773
+ } else {
774
+ let parent = node.parentNode;
775
+ while (parent) {
776
+ if (parent.localName === 'fieldset') {
777
+ break;
778
+ }
779
+ parent = parent.parentNode;
780
+ }
781
+ if (parent && parent.hasAttribute('disabled') &&
782
+ node.parentNode.localName !== 'legend') {
783
+ matched.push(node);
784
+ }
785
+ }
744
786
  }
745
787
  break;
746
788
  case 'enabled':
@@ -751,17 +793,75 @@ const matchPseudoClassSelector = (
751
793
  matched.push(node);
752
794
  }
753
795
  break;
796
+ case 'read-only':
797
+ if (/^(?:input|textarea)$/.test(localName)) {
798
+ if (node.hasAttribute('readonly') ||
799
+ node.hasAttribute('disabled')) {
800
+ matched.push(node);
801
+ }
802
+ } else if (!isContentEditable(node)) {
803
+ matched.push(node);
804
+ }
805
+ break;
806
+ case 'read-write':
807
+ if (/^(?:input|textarea)$/.test(localName)) {
808
+ if (!(node.hasAttribute('readonly') ||
809
+ node.hasAttribute('disabled'))) {
810
+ matched.push(node);
811
+ }
812
+ } else if (isContentEditable(node)) {
813
+ matched.push(node);
814
+ }
815
+ break;
816
+ case 'placeholder-shown':
817
+ if (/^(?:input|textarea)$/.test(localName) &&
818
+ node.hasAttribute('placeholder') &&
819
+ node.getAttribute('placeholder').trim().length &&
820
+ node.value === '') {
821
+ matched.push(node);
822
+ }
823
+ break;
754
824
  case 'checked':
755
- if ((/^input$/.test(localName) && node.hasAttribute('type') &&
825
+ if ((localName === 'input' && node.hasAttribute('type') &&
756
826
  /^(?:checkbox|radio)$/.test(node.getAttribute('type')) &&
757
827
  node.checked) ||
758
828
  (localName === 'option' && node.selected)) {
759
829
  matched.push(node);
760
830
  }
761
831
  break;
832
+ case 'indeterminate':
833
+ if ((localName === 'input' && node.type === 'checkbox' &&
834
+ node.indeterminate) ||
835
+ (localName === 'progress' && !node.hasAttribute('value'))) {
836
+ matched.push(node);
837
+ } else if (localName === 'input' && node.type === 'radio') {
838
+ const radioName = node.name;
839
+ let form = node;
840
+ while (form) {
841
+ if (form.localName === 'form') {
842
+ break;
843
+ }
844
+ form = form.parentNode;
845
+ }
846
+ if (form && radioName) {
847
+ const sel = `input[type="radio"][name="${radioName}"]`;
848
+ const arr = new Matcher(sel, form).querySelectorAll();
849
+ let checked;
850
+ for (const i of arr) {
851
+ checked = !!i.checked;
852
+ if (checked) {
853
+ break;
854
+ }
855
+ }
856
+ if (!checked) {
857
+ matched.push(node);
858
+ }
859
+ }
860
+ }
861
+ break;
762
862
  case 'default':
763
863
  // input[type="checkbox"], input[type="radio"]
764
- if (/^input$/.test(localName) && node.hasAttribute('type') &&
864
+ if (localName === 'input' && node.hasAttribute('type') &&
765
865
  /^(?:checkbox|radio)$/.test(node.getAttribute('type'))) {
766
866
  if (node.hasAttribute('checked')) {
767
867
  matched.push(node);
@@ -806,12 +906,45 @@ const matchPseudoClassSelector = (
806
906
  } else if ((localName === 'button' &&
807
907
  (!node.hasAttribute('type') ||
808
908
  node.getAttribute('type') === 'submit')) ||
809
- (/^input$/.test(localName) && node.hasAttribute('type') &&
909
+ (localName === 'input' && node.hasAttribute('type') &&
810
910
  /^(?:image|submit)$/.test(node.getAttribute('type')))) {
811
911
  throw new DOMException(`Unsupported pseudo-class ${astName}`,
812
912
  'NotSupportedError');
813
913
  }
814
914
  break;
915
+ case 'valid':
916
+ if (HTML_FORM_INPUT.test(localName) ||
917
+ /^(?:f(?:ieldset|orm)|button|output)$/.test(localName)) {
918
+ if (node.checkValidity()) {
919
+ matched.push(node);
920
+ }
921
+ }
922
+ break;
923
+ case 'invalid':
924
+ if (HTML_FORM_INPUT.test(localName) ||
925
+ /^(?:f(?:ieldset|orm)|button|output)$/.test(localName)) {
926
+ if (!node.checkValidity()) {
927
+ matched.push(node);
928
+ }
929
+ }
930
+ break;
931
+ case 'in-range':
932
+ if (localName === 'input' &&
933
+ node.hasAttribute('min') && node.hasAttribute('max')) {
934
+ if (!(node.validity.rangeUnderflow ||
935
+ node.validity.rangeOverflow)) {
936
+ matched.push(node);
937
+ }
938
+ }
939
+ break;
940
+ case 'out-of-range':
941
+ if (localName === 'input' &&
942
+ node.hasAttribute('min') && node.hasAttribute('max')) {
943
+ if (node.validity.rangeUnderflow || node.validity.rangeOverflow) {
944
+ matched.push(node);
945
+ }
946
+ }
947
+ break;
815
948
  case 'required':
816
949
  if (HTML_FORM_INPUT.test(localName) && node.required) {
817
950
  matched.push(node);
@@ -905,24 +1038,16 @@ const matchPseudoClassSelector = (
905
1038
  case 'fullscreen':
906
1039
  case 'future':
907
1040
  case 'hover':
908
- case 'indeterminate':
909
- case 'invalid':
910
- case 'in-range':
911
1041
  case 'modal':
912
1042
  case 'muted':
913
- case 'out-of-range':
914
1043
  case 'past':
915
1044
  case 'paused':
916
1045
  case 'picture-in-picture':
917
- case 'placeholder-shown':
918
1046
  case 'playing':
919
- case 'read-only':
920
- case 'read-write':
921
1047
  case 'seeking':
922
1048
  case 'stalled':
923
1049
  case 'user-invalid':
924
1050
  case 'user-valid':
925
- case 'valid':
926
1051
  case 'volume-locked':
927
1052
  throw new DOMException(`Unsupported pseudo-class ${astName}`,
928
1053
  'NotSupportedError');
@@ -1003,19 +1128,10 @@ class Matcher {
1003
1128
  * @returns {boolean} - result
1004
1129
  */
1005
1130
  _isAttached() {
1006
- const root = this.#document?.documentElement;
1007
- let bool;
1008
- if (root) {
1009
- let node = this.#node;
1010
- while (node) {
1011
- if (node === root) {
1012
- bool = true;
1013
- break;
1014
- }
1015
- node = node.parentNode;
1016
- }
1017
- }
1018
- return !!bool;
1131
+ const root = this.#document.documentElement;
1132
+ const posBit =
1133
+ this.#node.compareDocumentPosition(root) & DOCUMENT_POSITION_CONTAINS;
1134
+ return !!posBit;
1019
1135
  };
1020
1136
 
1021
1137
  /**
@@ -1587,6 +1703,7 @@ module.exports = {
1587
1703
  Matcher,
1588
1704
  collectNthChild,
1589
1705
  collectNthOfType,
1706
+ isContentEditable,
1590
1707
  matchAnPlusB,
1591
1708
  matchAttributeSelector,
1592
1709
  matchClassSelector,
@@ -28,6 +28,7 @@ export function collectNthOfType(anb?: {
28
28
  b: number;
29
29
  reverse?: boolean;
30
30
  }, node?: object): Array<object | undefined>;
31
+ export function isContentEditable(node?: object): boolean;
31
32
  export function matchAnPlusB(nthName: string, ast?: object, node?: object): Array<object | undefined>;
32
33
  export function matchAttributeSelector(ast?: object, node?: object): object | null;
33
34
  export function matchClassSelector(ast?: object, node?: object): object | null;