Unverified Commit 67ee3e19 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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 <base> 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 <a> 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.
parent b5c1b61c
...@@ -83,6 +83,26 @@ ...@@ -83,6 +83,26 @@
font-family: courier, lucidia; 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 */ /* Styles for the copy-to-clipboard button */
.copyable-container { .copyable-container {
position: relative; position: relative;
......
...@@ -57,6 +57,30 @@ function supportsCopying() { ...@@ -57,6 +57,30 @@ function supportsCopying() {
!!document.queryCommandSupported('copy'); !!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 // Copies the text inside the currently visible snippet to the clipboard, or the
// given element, if any. // given element, if any.
function copyTextToClipboard(element) { function copyTextToClipboard(element) {
......
{@inject-html} {@inject-html}
<a name="{{id}}"></a>
<div class="anchor-container">
<a class="anchor-button-overlay anchor-button" title="Copy link to clipboard"
onmouseenter="fixHref(this, '{{id}}');"
onclick="fixHref(this, '{{id}}'); copyStringToClipboard(this.href);"
href="#">
<i class="material-icons copy-image">link</i>
</a>
</div>
<div class="snippet-buttons"> <div class="snippet-buttons">
<script>var visibleSnippet{{serial}} = "shortSnippet{{serial}}";</script> <script>var visibleSnippet{{serial}} = "shortSnippet{{serial}}";</script>
<button id="shortSnippet{{serial}}Button" <button id="shortSnippet{{serial}}Button"
......
{@inject-html} {@inject-html}
<a name="{{id}}"></a>
<div class="anchor-container">
<a class="anchor-button-overlay anchor-button" title="Copy link to clipboard"
onmouseenter="fixHref(this, '{{id}}');"
onclick="fixHref(this, '{{id}}'); copyStringToClipboard(this.href);"
href="#">
<i class="material-icons copy-image">link</i>
</a>
</div>
<div class="snippet-buttons"> <div class="snippet-buttons">
<button id="shortSnippet{{serial}}Button" selected>Sample</button> <button id="shortSnippet{{serial}}Button" selected>Sample</button>
</div> </div>
......
...@@ -152,13 +152,13 @@ void main(List<String> argList) { ...@@ -152,13 +152,13 @@ void main(List<String> argList) {
input, input,
snippetType, snippetType,
template: template, template: template,
id: id.join('.'),
output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null, output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null,
metadata: <String, Object>{ metadata: <String, Object>{
'sourcePath': environment['SOURCE_PATH'], 'sourcePath': environment['SOURCE_PATH'],
'sourceLine': environment['SOURCE_LINE'] != null 'sourceLine': environment['SOURCE_LINE'] != null
? int.tryParse(environment['SOURCE_LINE']) ? int.tryParse(environment['SOURCE_LINE'])
: null, : null,
'id': id.join('.'),
'serial': serial, 'serial': serial,
'package': packageName, 'package': packageName,
'library': libraryName, 'library': libraryName,
......
...@@ -5,8 +5,9 @@ ...@@ -5,8 +5,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:dart_style/dart_style.dart'; import 'package:dart_style/dart_style.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'configuration.dart'; import 'configuration.dart';
...@@ -128,13 +129,12 @@ class SnippetGenerator { ...@@ -128,13 +129,12 @@ class SnippetGenerator {
'code': htmlEscape.convert(result.join('\n')), 'code': htmlEscape.convert(result.join('\n')),
'language': language ?? 'dart', 'language': language ?? 'dart',
'serial': '', 'serial': '',
'id': '', 'id': metadata['id'],
'app': '', 'app': '',
}; };
if (type == SnippetType.application) { if (type == SnippetType.application) {
substitutions substitutions
..['serial'] = metadata['serial'].toString() ?? '0' ..['serial'] = metadata['serial']?.toString() ?? '0'
..['id'] = injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent
..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent); ..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent);
} }
return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) { return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) {
...@@ -209,9 +209,15 @@ class SnippetGenerator { ...@@ -209,9 +209,15 @@ class SnippetGenerator {
/// The [id] is a string ID to use for the output file, and to tell the user /// 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 /// about in the `flutter create` hint. It must not be null if the [type] is
/// [SnippetType.application]. /// [SnippetType.application].
String generate(File input, SnippetType type, {String template, String id, File output, Map<String, Object> metadata}) { String generate(
File input,
SnippetType type, {
String template,
File output,
@required Map<String, Object> metadata,
}) {
assert(template != null || type != SnippetType.application); assert(template != null || type != SnippetType.application);
assert(id != null || type != SnippetType.application); assert(metadata != null && metadata['id'] != null);
assert(input != null); assert(input != null);
final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input)); final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input));
switch (type) { switch (type) {
...@@ -227,7 +233,6 @@ class SnippetGenerator { ...@@ -227,7 +233,6 @@ class SnippetGenerator {
'The template $template was not found in the templates directory ${templatesDir.path}'); 'The template $template was not found in the templates directory ${templatesDir.path}');
exit(1); exit(1);
} }
snippetData.add(_ComponentTuple('id', <String>[id]));
final String templateContents = _loadFileAsUtf8(templateFile); final String templateContents = _loadFileAsUtf8(templateFile);
String app = interpolateTemplate(snippetData, templateContents); String app = interpolateTemplate(snippetData, templateContents);
...@@ -239,7 +244,7 @@ class SnippetGenerator { ...@@ -239,7 +244,7 @@ class SnippetGenerator {
} }
snippetData.add(_ComponentTuple('app', app.split('\n'))); 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}'); stderr.writeln('Writing to ${outputFile.absolute.path}');
outputFile.writeAsStringSync(app); outputFile.writeAsStringSync(app);
...@@ -252,7 +257,7 @@ class SnippetGenerator { ...@@ -252,7 +257,7 @@ class SnippetGenerator {
); );
metadata ??= <String, Object>{}; metadata ??= <String, Object>{};
metadata.addAll(<String, Object>{ metadata.addAll(<String, Object>{
'id': id, 'id': metadata['id'],
'file': path.basename(outputFile.path), 'file': path.basename(outputFile.path),
'description': description?.mergedContent, 'description': description?.mergedContent,
}); });
......
...@@ -74,8 +74,14 @@ void main() { ...@@ -74,8 +74,14 @@ void main() {
``` ```
'''); ''');
final String html = final String html = generator.generate(
generator.generate(inputFile, SnippetType.application, template: 'template', id: 'id'); inputFile,
SnippetType.application,
template: 'template',
metadata: <String, Object>{
'id': 'id',
},
);
expect(html, contains('<div>HTML Bits</div>')); expect(html, contains('<div>HTML Bits</div>'));
expect(html, contains('<div>More HTML Bits</div>')); expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains('print(&#39;The actual \$name.&#39;);')); expect(html, contains('print(&#39;The actual \$name.&#39;);'));
...@@ -103,7 +109,7 @@ void main() { ...@@ -103,7 +109,7 @@ void main() {
``` ```
'''); ''');
final String html = generator.generate(inputFile, SnippetType.sample); final String html = generator.generate(inputFile, SnippetType.sample, metadata: <String, Object>{'id': 'id'});
expect(html, contains('<div>HTML Bits</div>')); expect(html, contains('<div>HTML Bits</div>'));
expect(html, contains('<div>More HTML Bits</div>')); expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains(' print(&#39;The actual \$name.&#39;);')); expect(html, contains(' print(&#39;The actual \$name.&#39;);'));
...@@ -135,9 +141,8 @@ void main() { ...@@ -135,9 +141,8 @@ void main() {
inputFile, inputFile,
SnippetType.application, SnippetType.application,
template: 'template', template: 'template',
id: 'id',
output: outputFile, output: outputFile,
metadata: <String, Object>{'sourcePath': 'some/path.dart'}, metadata: <String, Object>{'sourcePath': 'some/path.dart', 'id': 'id'},
); );
expect(expectedMetadataFile.existsSync(), isTrue); expect(expectedMetadataFile.existsSync(), isTrue);
final Map<String, dynamic> json = jsonDecode(expectedMetadataFile.readAsStringSync()); final Map<String, dynamic> json = jsonDecode(expectedMetadataFile.readAsStringSync());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment