From 67ee3e191e37199a0efdf5cf41990ae7258402f6 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 10 Jul 2019 16:48:20 -0700 Subject: [PATCH] Add anchors to samples (#35906) This adds an "anchor button" to each of the samples so that the user can link to individual samples instead of having to link to just the page. Clicking on the anchor button jumps to the anchor, as well as copying the anchor URL to the clipboard. There is some oddness in the implementation: because dartdoc uses a tag, the href for the link can't just be "#id", it has to calculate the URL from the current window href. I do that in the onmouseenter and onclick because onload doesn't get triggered for tags (and onmouseenter doesn't get triggered for mobile platforms), but I still want the href to be updated before someone right-clicks it to copy the URL. --- dev/docs/assets/snippets.css | 20 ++++++++++++++++ dev/docs/assets/snippets.js | 24 +++++++++++++++++++ .../config/skeletons/application.html | 9 +++++++ dev/snippets/config/skeletons/sample.html | 9 +++++++ dev/snippets/lib/main.dart | 2 +- dev/snippets/lib/snippets.dart | 23 +++++++++++------- dev/snippets/test/snippets_test.dart | 15 ++++++++---- 7 files changed, 87 insertions(+), 15 deletions(-) diff --git a/dev/docs/assets/snippets.css b/dev/docs/assets/snippets.css index 4fb200addb..e5ee48f170 100644 --- a/dev/docs/assets/snippets.css +++ b/dev/docs/assets/snippets.css @@ -83,6 +83,26 @@ font-family: courier, lucidia; } +.anchor-container { + position: relative; +} + +.anchor-button-overlay { + position: absolute; + top: 0px; + right: 5px; + height: 28px; + width: 28px; + transition: .3s ease; + background-color: #2372a3; +} + +.anchor-button { + border-style: none; + background: none; + cursor: pointer; +} + /* Styles for the copy-to-clipboard button */ .copyable-container { position: relative; diff --git a/dev/docs/assets/snippets.js b/dev/docs/assets/snippets.js index 9d4da921da..86e9a2c117 100644 --- a/dev/docs/assets/snippets.js +++ b/dev/docs/assets/snippets.js @@ -57,6 +57,30 @@ function supportsCopying() { !!document.queryCommandSupported('copy'); } +// Copies the given string to the clipboard. +function copyStringToClipboard(string) { + var textArea = document.createElement("textarea"); + textArea.value = string; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + if (!supportsCopying()) { + alert('Unable to copy to clipboard (not supported by browser)'); + return; + } + + try { + document.execCommand('copy'); + } finally { + document.body.removeChild(textArea); + } +} + +function fixHref(anchor, id) { + anchor.href = window.location.href.replace(/#.*$/, '') + '#' + id; +} + // Copies the text inside the currently visible snippet to the clipboard, or the // given element, if any. function copyTextToClipboard(element) { diff --git a/dev/snippets/config/skeletons/application.html b/dev/snippets/config/skeletons/application.html index 7d7e17bab4..56b648cc03 100644 --- a/dev/snippets/config/skeletons/application.html +++ b/dev/snippets/config/skeletons/application.html @@ -1,4 +1,13 @@ {@inject-html} + +
+ + link + +
diff --git a/dev/snippets/lib/main.dart b/dev/snippets/lib/main.dart index 786833d161..996b186c9b 100644 --- a/dev/snippets/lib/main.dart +++ b/dev/snippets/lib/main.dart @@ -152,13 +152,13 @@ void main(List argList) { input, snippetType, template: template, - id: id.join('.'), output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null, metadata: { 'sourcePath': environment['SOURCE_PATH'], 'sourceLine': environment['SOURCE_LINE'] != null ? int.tryParse(environment['SOURCE_LINE']) : null, + 'id': id.join('.'), 'serial': serial, 'package': packageName, 'library': libraryName, diff --git a/dev/snippets/lib/snippets.dart b/dev/snippets/lib/snippets.dart index 9f69daec2a..05d54f4c16 100644 --- a/dev/snippets/lib/snippets.dart +++ b/dev/snippets/lib/snippets.dart @@ -5,8 +5,9 @@ import 'dart:convert'; import 'dart:io'; -import 'package:path/path.dart' as path; import 'package:dart_style/dart_style.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; import 'configuration.dart'; @@ -128,13 +129,12 @@ class SnippetGenerator { 'code': htmlEscape.convert(result.join('\n')), 'language': language ?? 'dart', 'serial': '', - 'id': '', + 'id': metadata['id'], 'app': '', }; if (type == SnippetType.application) { substitutions - ..['serial'] = metadata['serial'].toString() ?? '0' - ..['id'] = injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent + ..['serial'] = metadata['serial']?.toString() ?? '0' ..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent); } return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) { @@ -209,9 +209,15 @@ class SnippetGenerator { /// The [id] is a string ID to use for the output file, and to tell the user /// about in the `flutter create` hint. It must not be null if the [type] is /// [SnippetType.application]. - String generate(File input, SnippetType type, {String template, String id, File output, Map metadata}) { + String generate( + File input, + SnippetType type, { + String template, + File output, + @required Map metadata, + }) { assert(template != null || type != SnippetType.application); - assert(id != null || type != SnippetType.application); + assert(metadata != null && metadata['id'] != null); assert(input != null); final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input)); switch (type) { @@ -227,7 +233,6 @@ class SnippetGenerator { 'The template $template was not found in the templates directory ${templatesDir.path}'); exit(1); } - snippetData.add(_ComponentTuple('id', [id])); final String templateContents = _loadFileAsUtf8(templateFile); String app = interpolateTemplate(snippetData, templateContents); @@ -239,7 +244,7 @@ class SnippetGenerator { } snippetData.add(_ComponentTuple('app', app.split('\n'))); - final File outputFile = output ?? getOutputFile(id); + final File outputFile = output ?? getOutputFile(metadata['id']); stderr.writeln('Writing to ${outputFile.absolute.path}'); outputFile.writeAsStringSync(app); @@ -252,7 +257,7 @@ class SnippetGenerator { ); metadata ??= {}; metadata.addAll({ - 'id': id, + 'id': metadata['id'], 'file': path.basename(outputFile.path), 'description': description?.mergedContent, }); diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart index 6408623cac..f3cb758a3c 100644 --- a/dev/snippets/test/snippets_test.dart +++ b/dev/snippets/test/snippets_test.dart @@ -74,8 +74,14 @@ void main() { ``` '''); - final String html = - generator.generate(inputFile, SnippetType.application, template: 'template', id: 'id'); + final String html = generator.generate( + inputFile, + SnippetType.application, + template: 'template', + metadata: { + 'id': 'id', + }, + ); expect(html, contains('
HTML Bits
')); expect(html, contains('
More HTML Bits
')); expect(html, contains('print('The actual \$name.');')); @@ -103,7 +109,7 @@ void main() { ``` '''); - final String html = generator.generate(inputFile, SnippetType.sample); + final String html = generator.generate(inputFile, SnippetType.sample, metadata: {'id': 'id'}); expect(html, contains('
HTML Bits
')); expect(html, contains('
More HTML Bits
')); expect(html, contains(' print('The actual \$name.');')); @@ -135,9 +141,8 @@ void main() { inputFile, SnippetType.application, template: 'template', - id: 'id', output: outputFile, - metadata: {'sourcePath': 'some/path.dart'}, + metadata: {'sourcePath': 'some/path.dart', 'id': 'id'}, ); expect(expectedMetadataFile.existsSync(), isTrue); final Map json = jsonDecode(expectedMetadataFile.readAsStringSync());