diff --git a/.changelog/graph-assets.md b/.changelog/graph-assets.md new file mode 100644 index 0000000..ab39788 --- /dev/null +++ b/.changelog/graph-assets.md @@ -0,0 +1,6 @@ +--- +category: fixed +type: patch +--- + +Preserves shared graph-page assets and initializes zoom controls after loading their dependency. Cancels pending zoom animation before resetting the diagram. diff --git a/.changelog/member-permalinks.md b/.changelog/member-permalinks.md new file mode 100644 index 0000000..6baa556 --- /dev/null +++ b/.changelog/member-permalinks.md @@ -0,0 +1,6 @@ +--- +category: fixed +type: patch +--- + +Scopes member tag and attribute IDs and replaces existing fragments in section permalinks. diff --git a/.changelog/search-reopen.md b/.changelog/search-reopen.md new file mode 100644 index 0000000..747d25f --- /dev/null +++ b/.changelog/search-reopen.md @@ -0,0 +1,6 @@ +--- +category: fixed +type: patch +--- + +Restores search results and active state when reopening an unchanged query. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 641180c..f83c71b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,8 @@ jobs: tools: composer:v2 - run: composer validate --strict - run: composer install --no-scripts --no-interaction --prefer-dist --no-progress + - name: Verify browser interaction contracts + run: node tests/interactions.test.cjs - name: Validate rendered checkout run: php tests/generate-fixture.php - name: Validate bundled consumer example diff --git a/class.html.twig b/class.html.twig index b936450..7f307e6 100644 --- a/class.html.twig +++ b/class.html.twig @@ -37,7 +37,7 @@
- + Properties {{ include('components/headerlink.html.twig', {'on': node, 'at': 'properties'}, with_context = false) }} @@ -52,7 +52,7 @@
- + Methods {{ include('components/headerlink.html.twig', {'on': node, 'at': 'methods'}, with_context = false) }} diff --git a/components/attributes.html.twig b/components/attributes.html.twig index a26d6b9..45e04ad 100644 --- a/components/attributes.html.twig +++ b/components/attributes.html.twig @@ -1,3 +1,5 @@ +{% set node_link_parts = link(node)|split('#') %} +{% set section_id = (node_link_parts|length > 1 ? node_link_parts|last ~ '-' : '') ~ 'attributes' %} {% if node is attributed %} {% set attributes = attributes(node)|specializedAttributes|sortByVisibility %} @@ -5,9 +7,9 @@
-
+
Attributes - {{ include('components/headerlink.html.twig', {'on': node, 'at': 'attributes'}, with_context = false) }} + {{ include('components/headerlink.html.twig', {'on': node, 'at': section_id}, with_context = false) }}
diff --git a/components/headerlink.html.twig b/components/headerlink.html.twig index 858aded..faa6bd1 100644 --- a/components/headerlink.html.twig +++ b/components/headerlink.html.twig @@ -1,5 +1,5 @@ 1 ? node_link_parts|last ~ '-' : '') ~ 'tags' %} {% set tags = node.tags|filter((v, k) => k not in ['var', 'param', 'property', 'property-read', 'property-write', 'method', 'return', 'package', 'api', 'deprecated']) %} {% if tags|length > 0 and tags|first|length > 0 %}
-
+
Tags - {{ include('components/headerlink.html.twig', {'on': node, 'at': 'tags'}, with_context = false) }} + {{ include('components/headerlink.html.twig', {'on': node, 'at': section_id}, with_context = false) }}
diff --git a/graphs/class.html.twig b/graphs/class.html.twig index c866fd2..87c9f2b 100644 --- a/graphs/class.html.twig +++ b/graphs/class.html.twig @@ -1,10 +1,52 @@ {% extends 'base.html.twig' %} {% block stylesheets %} + {{ parent() }} {% endblock %} {% block javascripts %} + {{ parent() }} + {% endblock %} {% block content %} @@ -49,42 +91,5 @@
- {% endblock %} diff --git a/js/search.js.twig b/js/search.js.twig index 31b5b4f..9b74167 100644 --- a/js/search.js.twig +++ b/js/search.js.twig @@ -170,10 +170,9 @@ var Search = (function () { searchField.setAttribute('placeholder', 'Search (Press "/" to focus)'); searchField.removeAttribute('disabled'); searchField.addEventListener('keyup', debounce(search, 250)); - searchField.addEventListener('focus', function () { + searchField.addEventListener('focus', function (event) { if (searchField.value.trim()) { - searchResults.classList.remove('phpdocumentor-search-results--hidden'); - searchResults.classList.add('show'); + search(event); } }); diff --git a/tests/fixture/src/Example.php b/tests/fixture/src/Example.php index f1f66d9..385329c 100644 --- a/tests/fixture/src/Example.php +++ b/tests/fixture/src/Example.php @@ -17,11 +17,19 @@ final class Example * echo $example->greet('Dash'); * ``` * + * @since 1.0.0 * @param string $name Reader name. * @return string A personalized greeting. */ + #[ExampleAttribute] public function greet(string $name): string { return 'Hello, ' . $name; } + /** @since 1.1.0 */ + #[ExampleAttribute] + public function farewell(): string + { + return 'Goodbye'; + } } diff --git a/tests/fixture/src/ExampleAttribute.php b/tests/fixture/src/ExampleAttribute.php new file mode 100644 index 0000000..bbe952d --- /dev/null +++ b/tests/fixture/src/ExampleAttribute.php @@ -0,0 +1,10 @@ + classes.add(x));}, remove(...xs) {xs.forEach(x => classes.delete(x));}, contains(x) {return classes.has(x);}}, addEventListener(n, f) {this.listeners[n] = f;}, setAttribute() {}, removeAttribute() {}, contains() {return false;}, value: '', innerHTML: '', blur() {}}; +} +const form = element(), field = element(), results = element(), entries = element(), close = element(), clear = element(), body = element(); +const nodes = {'[data-search-form]': form, '[data-search-form] input[type="search"]': field, '[data-search-results]': results, '[data-search-results] .phpdocumentor-search-results__entries': entries, '.phpdocumentor-search-results__close': close, '[data-search-clear]': clear}; +const documentListeners = {}, windowListeners = {}; +const context = {document: {querySelector: s => nodes[s], body, addEventListener(name, callback) {documentListeners[name] = callback;}, createElement() {return {set textContent(v) {this.innerHTML = v;}};}}, window: {addEventListener(name, callback) {windowListeners[name] = callback;}}, Fuse: function () {this.search = value => value === 'missing' ? [] : [{name: 'Example', url: 'classes/Example.html'}];}, setTimeout(f) {f(); return 1;}, clearTimeout() {}}; +vm.createContext(context); +vm.runInContext(fs.readFileSync(path.join(root, 'js/search.js.twig'), 'utf8'), context); +context.Search.init(); +const event = {target: field, stopPropagation() {}, preventDefault() {}}; +for (const query of ['Example', 'missing']) { + field.value = query; + field.listeners.keyup(event); + const original = entries.innerHTML; + assert.ok(original); + for (const dismiss of [() => close.listeners.click(event), () => documentListeners.click(event), () => windowListeners.keyup({...event, key: 'Escape'})]) { + dismiss(); + assert.equal(entries.innerHTML, ''); + field.listeners.focus(event); + assert.equal(entries.innerHTML, original, 'Reopening must regenerate unchanged-query results'); + assert.ok(body.classList.contains('phpdocumentor-search--active-body')); + assert.ok(form.classList.contains('phpdocumentor-search--has-results')); + assert.ok(results.classList.contains('show')); + } +} +clear.listeners.click(event); +assert.equal(field.value, ''); +assert.equal(entries.innerHTML, ''); +assert.ok(!results.classList.contains('show')); +console.log('PASS search reopen, empty results, state and clear'); +const graph = fs.readFileSync(path.join(root, 'graphs/class.html.twig'), 'utf8'); +const scripts = graph.match(/{% block javascripts %}([\s\S]*?){% endblock %}/)[1]; +assert.match(graph, /{% block stylesheets %}\s*{{ parent\(\) }}/); +assert.match(scripts, /{{ parent\(\) }}/); +assert.ok(scripts.indexOf('panzoom.min.js') < scripts.indexOf('var instance = panzoom')); +assert.ok(scripts.includes('var instance = panzoom'), 'Graph initialization must live after its footer dependency'); +const graphNodes = {}; +const calls = []; +let pendingZoom = null, scale = 1; +vm.runInNewContext(scripts.match(/