@abcnews/components-builder 0.0.2 → 0.0.3
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/README.md +76 -0
- package/dist/BuilderStyleRoot/BuilderStyleRoot.svelte +171 -169
- package/dist/GoogleDocScrollyteller/GoogleDocScrollyteller.stories.svelte +1 -0
- package/dist/GoogleDocScrollyteller/GoogleDocScrollyteller.svelte +8 -1
- package/dist/GoogleDocScrollyteller/GoogleDocScrollyteller.svelte.d.ts +2 -1
- package/dist/GoogleDocScrollyteller/utils.d.ts +3 -2
- package/dist/GoogleDocScrollyteller/utils.js +29 -29
- package/dist/MarkerAdmin/MarkerAdmin.svelte +417 -0
- package/dist/MarkerAdmin/MarkerAdmin.svelte.d.ts +7 -0
- package/dist/ScreenshotTool/ScreenshotTool.svelte +3 -5
- package/dist/ScreenshotTool/ScreenshotTool.svelte.d.ts +2 -2
- package/dist/UpdateChecker/UpdateChecker.svelte +3 -2
- package/dist/UpdateChecker/UpdateChecker.svelte.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ The update checker extracts the current version number from the URL, and recursi
|
|
|
16
16
|
|
|
17
17
|
Please ensure all your application state is kept in the URL, otherwise it may be lost on upgrade.
|
|
18
18
|
|
|
19
|
+
Please take care when releasing your projects. If the deploy fails and you skip a version this component will get stuck at that point. You must either rewrite your git history to delete and redeploy the missing version, or add an empty index.html on contentftp to trick the updater into skipping it.
|
|
20
|
+
|
|
21
|
+
To tnclude the update checker, use the following code with optional button text:
|
|
22
|
+
|
|
23
|
+
```svelte
|
|
24
|
+
<UpdateChecker buttonText="Open new builder" />
|
|
25
|
+
```
|
|
26
|
+
|
|
19
27
|
## ScreenshotTool (fallback images)
|
|
20
28
|
|
|
21
29
|
The screenshot tool allows users to paste an entire article, then extracts all the markers and sends them to a third-party service to screenshot.
|
|
@@ -24,6 +32,25 @@ For this to work you must set up a standalone page that can display the visualis
|
|
|
24
32
|
|
|
25
33
|
Note that the screenshot tool is not fast. We haven't been able to speed up the server side enough to make this a great experience.
|
|
26
34
|
|
|
35
|
+
```svelte
|
|
36
|
+
<ScreenshotTool
|
|
37
|
+
defaultMarkerName={() => "My marker"}
|
|
38
|
+
prefixes={{
|
|
39
|
+
"Scrolly mark": "#mark",
|
|
40
|
+
"Scrolly opener": "#scrollytellerNAMEelectionmap1",
|
|
41
|
+
"Inline graphic": "#graphicinline",
|
|
42
|
+
}}
|
|
43
|
+
iframeUrl={window.location.origin +
|
|
44
|
+
window.location.pathname.replace("/builder/", "/iframe/")}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `defaultMarkerName` Function provides a user-friendly version of the marker for the preview step. This is the same funcitopn as the `MarkerAdmin` component.
|
|
49
|
+
|
|
50
|
+
Prefixes correlate to the different marker types that have been implemented in the app. This is the same object as the `MarkerAdmin` component.
|
|
51
|
+
|
|
52
|
+
The iframe URL is the location that the screenshot tool hits to create screenshots. The screenshot tool will pass this iframe config via the hash, e.g. /iframe/#MARKER.
|
|
53
|
+
|
|
27
54
|
## Typeahead
|
|
28
55
|
|
|
29
56
|
The typeahead component uses a native HTML input element, in conjunction with the datalist element, to provide native searching and keyboard accessibility. The component implements its own multi-select functionality as that isn't available natively.
|
|
@@ -95,6 +122,55 @@ This uses the native dialogue element, so focus will always be inside the modal.
|
|
|
95
122
|
<Modal title="Example modal" {children} {footerChildren} />
|
|
96
123
|
```
|
|
97
124
|
|
|
125
|
+
## Google Doc Scrollyteller
|
|
126
|
+
|
|
127
|
+
This component lets edits draft stories in Google Docs and preview to scroll teller in real time. Period this is useful because multiple people can be editing at once and have a real-time preview, whereas the CMS is single user and can be slow to iterate on.
|
|
128
|
+
|
|
129
|
+
This component must be set up on its own page, as it has two different routes built into it. An example implementation follows:
|
|
130
|
+
|
|
131
|
+
```svelte
|
|
132
|
+
<script>
|
|
133
|
+
import {
|
|
134
|
+
BuilderStyleRoot,
|
|
135
|
+
GoogleDocScrollyteller,
|
|
136
|
+
} from "@abcnews/components-builder";
|
|
137
|
+
import { loadScrollyteller } from "@abcnews/svelte-scrollyteller";
|
|
138
|
+
import MyScrollyteller from "../components/MyScrollyteller.svelte";
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<BuilderStyleRoot>
|
|
142
|
+
<GoogleDocScrollyteller
|
|
143
|
+
name="electionmap"
|
|
144
|
+
{loadScrollyteller}
|
|
145
|
+
ScrollytellerRoot={MyScrollyteller}
|
|
146
|
+
/>
|
|
147
|
+
</BuilderStyleRoot>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
You must pass in the loadScrollyteller function, as well as your component that implements svelte-scrollyteller. When the Google doc is loaded, your component will be mounted with the relevant markup in-page.
|
|
151
|
+
|
|
152
|
+
## MarkerAdmin
|
|
153
|
+
|
|
154
|
+
This is a component that streamlines how you handle markers. It includes a copy and paste function as well as the ability to save and load markers from localstorage.
|
|
155
|
+
|
|
156
|
+
As a prerequisite your builder must store its state in window.location.hash. E.g. `/builder/#marker`.
|
|
157
|
+
|
|
158
|
+
```svelte
|
|
159
|
+
<MarkerAdmin
|
|
160
|
+
projectName="elections-federal2025-lower-house"
|
|
161
|
+
prefixes={{
|
|
162
|
+
"Scrolly mark": "#mark",
|
|
163
|
+
"Scrolly opener": "#scrollytellerNAMEelectionmap1",
|
|
164
|
+
"Inline graphic": "#graphicinline",
|
|
165
|
+
}}
|
|
166
|
+
defaultMarkerName={() => "My marker"}
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`defaultMarkerName` is a function that returns a user friendly name for the marker when the user clicks the save button. You can use this to customise default marker names based on the current hash, which can be useful if you have several different visualisations with distinct names.
|
|
171
|
+
|
|
172
|
+
Prefixes correlate to the different marker types that have been implemented in the app. Users can choose which type of marker they want to copy. This is the same object as the `MarkerAdmin` component.
|
|
173
|
+
|
|
98
174
|
## Developing
|
|
99
175
|
|
|
100
176
|
Once you've nstalled dependencies with `npm install`, start a development storybook:
|
|
@@ -1,175 +1,177 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
let { children } = $props();
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<div class="builder-style-root">
|
|
6
|
-
|
|
6
|
+
{@render children?.()}
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
9
|
+
<svelte:head>
|
|
10
|
+
<style>
|
|
11
|
+
.builder-style-root {
|
|
12
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
font-weight: 400;
|
|
15
|
+
|
|
16
|
+
--text: #222;
|
|
17
|
+
--text-light: #888;
|
|
18
|
+
--background: #fff;
|
|
19
|
+
--border: rgba(122, 123, 135, 0.5);
|
|
20
|
+
--background-alt: #f2f4f5;
|
|
21
|
+
|
|
22
|
+
color: var(--text);
|
|
23
|
+
background-color: var(--background);
|
|
24
|
+
|
|
25
|
+
font-synthesis: none;
|
|
26
|
+
text-rendering: optimizeLegibility;
|
|
27
|
+
-webkit-font-smoothing: antialiased;
|
|
28
|
+
-moz-osx-font-smoothing: grayscale;
|
|
29
|
+
-webkit-text-size-adjust: 100%;
|
|
30
|
+
|
|
31
|
+
color-scheme: dark light;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
.builder-style-root {
|
|
36
|
+
--text: #ccc;
|
|
37
|
+
--text-light: #888;
|
|
38
|
+
--background: #1a1a1a;
|
|
39
|
+
--background-alt: #2c2c2f;
|
|
40
|
+
--border: rgba(122, 123, 135, 0.5);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* // builder-style-root */
|
|
45
|
+
.builder-style-root {
|
|
46
|
+
* {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
}
|
|
49
|
+
fieldset {
|
|
50
|
+
margin-bottom: 1rem;
|
|
51
|
+
padding: var(--padding);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
position: relative;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 0.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.fieldset {
|
|
60
|
+
padding: 0 var(--padding);
|
|
61
|
+
border: 1px solid transparent;
|
|
62
|
+
}
|
|
63
|
+
.fieldset,
|
|
64
|
+
fieldset {
|
|
65
|
+
--padding: 0.75rem;
|
|
66
|
+
margin-bottom: 1rem;
|
|
67
|
+
border-radius: 0.2rem;
|
|
68
|
+
}
|
|
69
|
+
fieldset.builder__spacious,
|
|
70
|
+
.fieldset.builder__spacious {
|
|
71
|
+
--padding: 1rem;
|
|
72
|
+
legend {
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.builder__inline {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-wrap: wrap;
|
|
80
|
+
gap: 0.5rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.buttons {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: 2px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
label {
|
|
90
|
+
margin-bottom: 0.5rem;
|
|
91
|
+
position: relative;
|
|
92
|
+
}
|
|
93
|
+
label span {
|
|
94
|
+
display: block;
|
|
95
|
+
margin-bottom: 0.3rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
select,
|
|
99
|
+
button,
|
|
100
|
+
input,
|
|
101
|
+
textarea {
|
|
102
|
+
padding: 0.25rem 0.5rem;
|
|
103
|
+
background: var(--background);
|
|
104
|
+
border: 1px solid var(--border);
|
|
105
|
+
color: var(--text);
|
|
106
|
+
border-radius: 0.2rem;
|
|
107
|
+
}
|
|
108
|
+
select:not([multiple]),
|
|
109
|
+
button:not(:disabled) {
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
&:hover,
|
|
112
|
+
&:focus-visible {
|
|
113
|
+
border-color: var(--text);
|
|
114
|
+
background: Highlight;
|
|
115
|
+
color: HighlightText;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
button:disabled {
|
|
119
|
+
color: var(--text-light);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
select,
|
|
123
|
+
input[type="text"],
|
|
124
|
+
input[type="password"],
|
|
125
|
+
textarea {
|
|
126
|
+
width: 100%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.btn-icon {
|
|
130
|
+
padding: 0;
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
height: 1.5rem;
|
|
133
|
+
width: 1.5rem;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
align-items: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.builder__submit-row {
|
|
139
|
+
text-align: right;
|
|
140
|
+
border-top: 1px solid var(--border);
|
|
141
|
+
padding: 0.5rem 0;
|
|
142
|
+
}
|
|
143
|
+
hr {
|
|
144
|
+
width: 100%;
|
|
145
|
+
border: none;
|
|
146
|
+
border-bottom: 1px solid var(--border);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
table.builder__table {
|
|
150
|
+
border-collapse: separate;
|
|
151
|
+
border: solid var(--border) 1px;
|
|
152
|
+
border-radius: 0.2rem;
|
|
153
|
+
border-spacing: 0;
|
|
154
|
+
|
|
155
|
+
td,
|
|
156
|
+
th {
|
|
157
|
+
border-left: solid var(--border) 1px;
|
|
158
|
+
border-top: solid var(--border) 1px;
|
|
159
|
+
padding: 0.25rem 0.5rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
th {
|
|
163
|
+
background-color: var(--background-alt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
th {
|
|
167
|
+
border-top: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
td:first-child,
|
|
171
|
+
th:first-child {
|
|
172
|
+
border-left: none;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
177
|
+
</svelte:head>
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
name = "myscrollyteller",
|
|
12
12
|
markerName = "mark",
|
|
13
13
|
ScrollytellerRoot,
|
|
14
|
+
loadScrollyteller,
|
|
14
15
|
}: {
|
|
15
16
|
name: string;
|
|
16
|
-
markerName
|
|
17
|
+
markerName?: string;
|
|
17
18
|
ScrollytellerRoot: Component;
|
|
19
|
+
loadScrollyteller: (
|
|
20
|
+
name?: string,
|
|
21
|
+
className?: string,
|
|
22
|
+
markerName?: string,
|
|
23
|
+
) => any;
|
|
18
24
|
} = $props();
|
|
19
25
|
|
|
20
26
|
const localStorageKey = `ABC_NEWS_BUILDER_GDOC_PREVIEW`;
|
|
@@ -63,6 +69,7 @@
|
|
|
63
69
|
name,
|
|
64
70
|
url: doc,
|
|
65
71
|
markerName,
|
|
72
|
+
loadScrollyteller,
|
|
66
73
|
})
|
|
67
74
|
.then((data) => {
|
|
68
75
|
title = data.title;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
name: string;
|
|
4
|
-
markerName
|
|
4
|
+
markerName?: string;
|
|
5
5
|
ScrollytellerRoot: Component;
|
|
6
|
+
loadScrollyteller: (name?: string, className?: string, markerName?: string) => any;
|
|
6
7
|
};
|
|
7
8
|
declare const GoogleDocScrollyteller: Component<$$ComponentProps, {}, "">;
|
|
8
9
|
type GoogleDocScrollyteller = ReturnType<typeof GoogleDocScrollyteller>;
|
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
* @param {function} [postprocessScrollytellerDefinition] -
|
|
9
9
|
* @returns
|
|
10
10
|
*/
|
|
11
|
-
export declare function loadData({ name, className, markerName, url, preprocessCoreEl, postprocessScrollytellerDefinition }: {
|
|
11
|
+
export declare function loadData({ name, className, markerName, url, preprocessCoreEl, postprocessScrollytellerDefinition, loadScrollyteller, }: {
|
|
12
12
|
name: any;
|
|
13
13
|
className?: string | undefined;
|
|
14
14
|
markerName?: string | undefined;
|
|
15
15
|
url: any;
|
|
16
16
|
preprocessCoreEl?: ((el: any) => any) | undefined;
|
|
17
17
|
postprocessScrollytellerDefinition?: ((a: any) => any) | undefined;
|
|
18
|
+
loadScrollyteller: any;
|
|
18
19
|
}): Promise<{
|
|
19
20
|
title: string | null | undefined;
|
|
20
21
|
coreText: string;
|
|
21
22
|
coreHTML: string;
|
|
22
|
-
scrollytellerDefinition:
|
|
23
|
+
scrollytellerDefinition: any;
|
|
23
24
|
} | undefined>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { loadScrollyteller } from '@abcnews/svelte-scrollyteller';
|
|
2
1
|
function mountTextToMountEl(mountText) {
|
|
3
|
-
const mountEl = document.createElement(
|
|
4
|
-
mountEl.setAttribute(
|
|
5
|
-
mountEl.setAttribute(
|
|
6
|
-
mountEl.setAttribute(
|
|
2
|
+
const mountEl = document.createElement("div");
|
|
3
|
+
mountEl.setAttribute("data-mount", "");
|
|
4
|
+
mountEl.setAttribute("data-component", "Anchor");
|
|
5
|
+
mountEl.setAttribute("id", mountText.slice(1));
|
|
7
6
|
return mountEl;
|
|
8
7
|
}
|
|
9
8
|
/**
|
|
@@ -16,55 +15,56 @@ function mountTextToMountEl(mountText) {
|
|
|
16
15
|
* @param {function} [postprocessScrollytellerDefinition] -
|
|
17
16
|
* @returns
|
|
18
17
|
*/
|
|
19
|
-
export async function loadData({ name, className =
|
|
18
|
+
export async function loadData({ name, className = "u-full", markerName = "mark", url, preprocessCoreEl = (el) => el, postprocessScrollytellerDefinition = (a) => a, loadScrollyteller, }) {
|
|
20
19
|
if (!url) {
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
23
|
-
const pubURL = url.replace(/\/[^/]+?$/,
|
|
24
|
-
const html = await fetch(pubURL).then(response => response.text());
|
|
25
|
-
const dom = new DOMParser().parseFromString(html,
|
|
26
|
-
const body = dom.querySelector(
|
|
27
|
-
const title = dom.querySelector(
|
|
22
|
+
const pubURL = url.replace(/\/[^/]+?$/, "/pub");
|
|
23
|
+
const html = await fetch(pubURL).then((response) => response.text());
|
|
24
|
+
const dom = new DOMParser().parseFromString(html, "text/html");
|
|
25
|
+
const body = dom.querySelector("#contents > div");
|
|
26
|
+
const title = dom.querySelector("title")?.textContent;
|
|
28
27
|
if (!body) {
|
|
29
|
-
throw new Error(
|
|
28
|
+
throw new Error("Body not found");
|
|
30
29
|
}
|
|
31
|
-
Array.from(body.querySelectorAll(
|
|
32
|
-
el.removeAttribute(
|
|
33
|
-
el.removeAttribute(
|
|
30
|
+
Array.from(body.querySelectorAll("*")).forEach((el) => {
|
|
31
|
+
el.removeAttribute("class");
|
|
32
|
+
el.removeAttribute("id");
|
|
34
33
|
});
|
|
35
34
|
const coreEls = Array.from(body.children).map(preprocessCoreEl);
|
|
36
35
|
const coreText = coreEls.reduce((memo, el) => {
|
|
37
36
|
const text = String(el.textContent).trim();
|
|
38
|
-
memo = `${memo}\n${text ? `\n${text}` :
|
|
37
|
+
memo = `${memo}\n${text ? `\n${text}` : ""}`;
|
|
39
38
|
return memo;
|
|
40
|
-
},
|
|
39
|
+
}, "");
|
|
41
40
|
const coreHTML = coreEls.reduce((memo, el) => {
|
|
42
41
|
const text = String(el.textContent).trim();
|
|
43
42
|
const html = el.outerHTML;
|
|
44
|
-
memo = `${memo}${text ? `${html}` :
|
|
43
|
+
memo = `${memo}${text ? `${html}` : ""}`;
|
|
45
44
|
return memo;
|
|
46
|
-
},
|
|
45
|
+
}, "");
|
|
47
46
|
const { scrollytellingEls } = coreEls.reduce((memo, el) => {
|
|
48
47
|
if (memo.hasEnded) {
|
|
49
48
|
return memo;
|
|
50
49
|
}
|
|
51
50
|
const text = String(el.textContent).trim();
|
|
52
|
-
if (text.indexOf(
|
|
51
|
+
if (text.indexOf("#remove") === 0) {
|
|
53
52
|
memo.isRemoving = true;
|
|
54
53
|
}
|
|
55
|
-
else if (text.indexOf(
|
|
54
|
+
else if (text.indexOf("#endremove") === 0) {
|
|
56
55
|
memo.isRemoving = false;
|
|
57
56
|
}
|
|
58
|
-
else if (text.indexOf(
|
|
59
|
-
if (text.indexOf(`#scrollyteller${name ? `NAME${name}` :
|
|
57
|
+
else if (text.indexOf("#") === 0) {
|
|
58
|
+
if (text.indexOf(`#scrollyteller${name ? `NAME${name}` : ""}`) === 0 &&
|
|
59
|
+
!memo.hasBegun) {
|
|
60
60
|
memo.hasBegun = true;
|
|
61
61
|
}
|
|
62
|
-
else if (text.indexOf(
|
|
62
|
+
else if (text.indexOf("#endscrollyteller") === 0) {
|
|
63
63
|
memo.hasEnded = true;
|
|
64
64
|
}
|
|
65
65
|
memo.scrollytellingEls.push(mountTextToMountEl(text));
|
|
66
66
|
}
|
|
67
|
-
else if (!memo.hasBegun || memo.isRemoving || text ===
|
|
67
|
+
else if (!memo.hasBegun || memo.isRemoving || text === "") {
|
|
68
68
|
// skip
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
@@ -75,10 +75,10 @@ export async function loadData({ name, className = 'u-full', markerName = 'mark'
|
|
|
75
75
|
hasBegun: false,
|
|
76
76
|
hasEnded: false,
|
|
77
77
|
isRemoving: false,
|
|
78
|
-
scrollytellingEls: []
|
|
78
|
+
scrollytellingEls: [],
|
|
79
79
|
});
|
|
80
|
-
const container = document.createElement(
|
|
81
|
-
scrollytellingEls.forEach(scrollytellingEl => container.appendChild(scrollytellingEl));
|
|
80
|
+
const container = document.createElement("div");
|
|
81
|
+
scrollytellingEls.forEach((scrollytellingEl) => container.appendChild(scrollytellingEl));
|
|
82
82
|
document.body.appendChild(container);
|
|
83
83
|
let scrollytellerDefinition = loadScrollyteller(name, className, markerName);
|
|
84
84
|
document.body.removeChild(container);
|
|
@@ -89,6 +89,6 @@ export async function loadData({ name, className = 'u-full', markerName = 'mark'
|
|
|
89
89
|
title,
|
|
90
90
|
coreText,
|
|
91
91
|
coreHTML,
|
|
92
|
-
scrollytellerDefinition
|
|
92
|
+
scrollytellerDefinition,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
|
+
import { slide } from "svelte/transition";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
/** Unique project name used in localStorage (e.g. "elections-federal2025-lower-house")*/
|
|
7
|
+
projectName = "dev",
|
|
8
|
+
defaultMarkerName = () => "",
|
|
9
|
+
prefixes = {
|
|
10
|
+
"Scrolly mark": "#mark",
|
|
11
|
+
"Scrolly opener": "#scrollytellerNAMEscrolly1",
|
|
12
|
+
"Standalone graphic": "#graphic",
|
|
13
|
+
},
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
type Marker = {
|
|
17
|
+
/** human readable name */
|
|
18
|
+
name: string;
|
|
19
|
+
/** actual marker string */
|
|
20
|
+
hash: string;
|
|
21
|
+
/** Date at which the marker was deleted (for undo) */
|
|
22
|
+
deleted?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let hasLoaded = $state(false);
|
|
26
|
+
let markers = $state<Marker[]>([]);
|
|
27
|
+
let mode = $state(Object.keys(prefixes)?.[0]);
|
|
28
|
+
|
|
29
|
+
/** which button should show the success indicator */
|
|
30
|
+
let successIndicator = $state("");
|
|
31
|
+
|
|
32
|
+
const [version = "0.0.0"] =
|
|
33
|
+
window.location.pathname
|
|
34
|
+
.match(/\/news-projects\/[^/]+\/(\d+\.\d+\.\d+)/)
|
|
35
|
+
?.slice(1) || [];
|
|
36
|
+
const localStorageKey = `ABC_NEWS_BUILDER_${projectName.toUpperCase().replace(/-/g, "_")}`;
|
|
37
|
+
|
|
38
|
+
/** Load & sanitise markers from localStorage on initial load */
|
|
39
|
+
$effect(() => {
|
|
40
|
+
let _markers = markers;
|
|
41
|
+
if (!hasLoaded) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
localStorage[localStorageKey] = JSON.stringify({
|
|
46
|
+
version: version,
|
|
47
|
+
lastUpdated: new Date().toISOString(),
|
|
48
|
+
expiry: 2026,
|
|
49
|
+
markers: _markers,
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
alert("Could not save markers: " + (e as Error).message);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/* ---------------------------------------------------------------------------
|
|
57
|
+
* slightly more complex delete logic than I hoped. When an item is marked
|
|
58
|
+
* as deleted, set a timer and actually delete it after ~5sec.
|
|
59
|
+
* don't delete while the user is still hovering the items.
|
|
60
|
+
*/
|
|
61
|
+
let isHovering = $state(false);
|
|
62
|
+
let interval = $state<NodeJS.Timeout>();
|
|
63
|
+
function clearDeleteCleanup() {
|
|
64
|
+
const _interval = untrack(() => interval);
|
|
65
|
+
if (_interval) {
|
|
66
|
+
clearInterval(_interval);
|
|
67
|
+
interval = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const _markers = untrack(() => markers);
|
|
72
|
+
|
|
73
|
+
clearDeleteCleanup();
|
|
74
|
+
if (isHovering) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interval = setInterval(() => {
|
|
79
|
+
const goodMarkers = (marker) =>
|
|
80
|
+
!marker.deleted || marker.deleted > Date.now() - 5000;
|
|
81
|
+
if (!_markers.every(goodMarkers)) {
|
|
82
|
+
markers = _markers.filter(goodMarkers);
|
|
83
|
+
}
|
|
84
|
+
}, 3000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
onMount(function load() {
|
|
88
|
+
try {
|
|
89
|
+
const storage = localStorage[localStorageKey];
|
|
90
|
+
if (!storage) {
|
|
91
|
+
hasLoaded = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const parsedStorage = JSON.parse(storage);
|
|
95
|
+
markers = parsedStorage.markers;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
alert("Could not load saved markers: " + (e as Error).message);
|
|
98
|
+
}
|
|
99
|
+
hasLoaded = true;
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
clearDeleteCleanup();
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
{#snippet saveIcon()}
|
|
108
|
+
<svg
|
|
109
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
110
|
+
width="16"
|
|
111
|
+
height="16"
|
|
112
|
+
fill="currentColor"
|
|
113
|
+
class="bi bi-floppy"
|
|
114
|
+
viewBox="0 0 16 16"
|
|
115
|
+
>
|
|
116
|
+
<title>Save</title>
|
|
117
|
+
<path d="M11 2H9v3h2z" />
|
|
118
|
+
<path
|
|
119
|
+
d="M1.5 0h11.586a1.5 1.5 0 0 1 1.06.44l1.415 1.414A1.5 1.5 0 0 1 16 2.914V14.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13A1.5 1.5 0 0 1 1.5 0M1 1.5v13a.5.5 0 0 0 .5.5H2v-4.5A1.5 1.5 0 0 1 3.5 9h9a1.5 1.5 0 0 1 1.5 1.5V15h.5a.5.5 0 0 0 .5-.5V2.914a.5.5 0 0 0-.146-.353l-1.415-1.415A.5.5 0 0 0 13.086 1H13v4.5A1.5 1.5 0 0 1 11.5 7h-7A1.5 1.5 0 0 1 3 5.5V1H1.5a.5.5 0 0 0-.5.5m3 4a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5V1H4zM3 15h10v-4.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5z"
|
|
120
|
+
/>
|
|
121
|
+
</svg>
|
|
122
|
+
{/snippet}
|
|
123
|
+
|
|
124
|
+
{#snippet copyIcon()}
|
|
125
|
+
<svg
|
|
126
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
127
|
+
width="16"
|
|
128
|
+
height="16"
|
|
129
|
+
fill="currentColor"
|
|
130
|
+
class="bi bi-copy"
|
|
131
|
+
viewBox="0 0 16 16"
|
|
132
|
+
>
|
|
133
|
+
<title>Copy to clipboard</title>
|
|
134
|
+
<path
|
|
135
|
+
fill-rule="evenodd"
|
|
136
|
+
d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"
|
|
137
|
+
/>
|
|
138
|
+
</svg>
|
|
139
|
+
{/snippet}
|
|
140
|
+
|
|
141
|
+
{#snippet pasteIcon()}
|
|
142
|
+
<svg
|
|
143
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
144
|
+
width="16"
|
|
145
|
+
height="16"
|
|
146
|
+
fill="currentColor"
|
|
147
|
+
class="bi bi-clipboard"
|
|
148
|
+
viewBox="0 0 16 16"
|
|
149
|
+
>
|
|
150
|
+
<title>Paste marker from clipboard</title>
|
|
151
|
+
<path
|
|
152
|
+
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"
|
|
153
|
+
/>
|
|
154
|
+
<path
|
|
155
|
+
d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"
|
|
156
|
+
/>
|
|
157
|
+
</svg>
|
|
158
|
+
{/snippet}
|
|
159
|
+
|
|
160
|
+
{#snippet deleteIcon()}
|
|
161
|
+
<svg
|
|
162
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
163
|
+
width="16"
|
|
164
|
+
height="16"
|
|
165
|
+
fill="currentColor"
|
|
166
|
+
class="bi bi-trash"
|
|
167
|
+
viewBox="0 0 16 16"
|
|
168
|
+
>
|
|
169
|
+
<path
|
|
170
|
+
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"
|
|
171
|
+
/>
|
|
172
|
+
<path
|
|
173
|
+
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"
|
|
174
|
+
/>
|
|
175
|
+
</svg>
|
|
176
|
+
{/snippet}
|
|
177
|
+
|
|
178
|
+
{#snippet undeleteIcon()}
|
|
179
|
+
<svg
|
|
180
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
181
|
+
width="16"
|
|
182
|
+
height="16"
|
|
183
|
+
fill="currentColor"
|
|
184
|
+
class="bi bi-arrow-counterclockwise"
|
|
185
|
+
viewBox="0 0 16 16"
|
|
186
|
+
>
|
|
187
|
+
<path
|
|
188
|
+
fill-rule="evenodd"
|
|
189
|
+
d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"
|
|
190
|
+
/>
|
|
191
|
+
<path
|
|
192
|
+
d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466"
|
|
193
|
+
/>
|
|
194
|
+
</svg>
|
|
195
|
+
{/snippet}
|
|
196
|
+
|
|
197
|
+
<div class="toolbar">
|
|
198
|
+
<select bind:value={mode}>
|
|
199
|
+
{#each Object.keys(prefixes) as prefix}
|
|
200
|
+
<option>{prefix}</option>
|
|
201
|
+
{/each}
|
|
202
|
+
</select>
|
|
203
|
+
<button
|
|
204
|
+
title="Copy marker"
|
|
205
|
+
class:success={successIndicator === "copy"}
|
|
206
|
+
onanimationend={() => {
|
|
207
|
+
successIndicator = "";
|
|
208
|
+
}}
|
|
209
|
+
onclick={(e) => {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
const hash = window.location.hash.slice(1);
|
|
212
|
+
navigator.clipboard.writeText(prefixes[mode] + hash);
|
|
213
|
+
successIndicator = "copy";
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{@render copyIcon()}
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
title="Paste marker from clipboard"
|
|
220
|
+
class:success={successIndicator === "paste"}
|
|
221
|
+
onanimationend={() => {
|
|
222
|
+
successIndicator = "";
|
|
223
|
+
}}
|
|
224
|
+
onclick={async (e) => {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
|
|
227
|
+
let text: string | null = await navigator.clipboard
|
|
228
|
+
.readText()
|
|
229
|
+
.catch((e) => {
|
|
230
|
+
console.error("Could not read clipboard");
|
|
231
|
+
return null;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!text) {
|
|
235
|
+
text = prompt("Paste a marker here to import its configuration");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!text) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let sanitisedMarker = text;
|
|
243
|
+
Object.keys(prefixes).forEach(
|
|
244
|
+
(prefix) => (sanitisedMarker = sanitisedMarker.replace(prefix, "")),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (window.location.hash.slice(1) === sanitisedMarker) {
|
|
248
|
+
alert(
|
|
249
|
+
"Pasted marker is the same as the current state. No changes were necessary.",
|
|
250
|
+
);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
window.location.hash = sanitisedMarker;
|
|
255
|
+
|
|
256
|
+
successIndicator = "paste";
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{@render pasteIcon()}
|
|
260
|
+
</button>
|
|
261
|
+
<div class="divider"></div>
|
|
262
|
+
<button
|
|
263
|
+
title="Save marker snapshot"
|
|
264
|
+
onclick={(e) => {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
const name = prompt(
|
|
267
|
+
"What would you like to call this snapshot?",
|
|
268
|
+
defaultMarkerName(),
|
|
269
|
+
);
|
|
270
|
+
if (!name) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
markers.push({
|
|
274
|
+
name,
|
|
275
|
+
hash: window.location.hash.slice(1),
|
|
276
|
+
});
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{@render saveIcon()}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<svelte:window
|
|
284
|
+
onblur={() => {
|
|
285
|
+
isHovering = false;
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
|
|
289
|
+
<table
|
|
290
|
+
class="saved-markers"
|
|
291
|
+
onmouseenter={() => {
|
|
292
|
+
isHovering = true;
|
|
293
|
+
}}
|
|
294
|
+
onmouseleave={() => {
|
|
295
|
+
isHovering = false;
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
<tbody>
|
|
299
|
+
{#each markers as marker, index}
|
|
300
|
+
<tr
|
|
301
|
+
class="row"
|
|
302
|
+
class:row--deleted={marker.deleted}
|
|
303
|
+
transition:slide
|
|
304
|
+
onclick={(e) =>
|
|
305
|
+
//@ts-ignore
|
|
306
|
+
e.target?.querySelector("a")?.click()}
|
|
307
|
+
>
|
|
308
|
+
<td class="name">
|
|
309
|
+
{#if marker.deleted}
|
|
310
|
+
{marker.name}
|
|
311
|
+
{:else}
|
|
312
|
+
<a href={`#${marker.hash}`}>
|
|
313
|
+
{marker.name}
|
|
314
|
+
</a>
|
|
315
|
+
{/if}
|
|
316
|
+
</td>
|
|
317
|
+
<td class="buttons">
|
|
318
|
+
<button
|
|
319
|
+
onclick={(e) => {
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
const newMarkers = [...markers];
|
|
322
|
+
const marker = newMarkers[index];
|
|
323
|
+
if (marker.deleted) {
|
|
324
|
+
delete marker.deleted;
|
|
325
|
+
} else {
|
|
326
|
+
marker.deleted = Date.now();
|
|
327
|
+
}
|
|
328
|
+
}}
|
|
329
|
+
title={marker.deleted ? "Undo delete" : "Delete marker"}
|
|
330
|
+
style:height="32px"
|
|
331
|
+
>
|
|
332
|
+
{#if marker.deleted}
|
|
333
|
+
{@render undeleteIcon()}
|
|
334
|
+
{:else}
|
|
335
|
+
{@render deleteIcon()}
|
|
336
|
+
{/if}
|
|
337
|
+
</button>
|
|
338
|
+
</td>
|
|
339
|
+
</tr>
|
|
340
|
+
{/each}
|
|
341
|
+
</tbody>
|
|
342
|
+
</table>
|
|
343
|
+
|
|
344
|
+
<style>
|
|
345
|
+
.toolbar {
|
|
346
|
+
display: flex;
|
|
347
|
+
justify-content: space-between;
|
|
348
|
+
gap: 0.5rem;
|
|
349
|
+
|
|
350
|
+
padding-bottom: 0.5rem;
|
|
351
|
+
border-bottom: 1px solid var(--border);
|
|
352
|
+
|
|
353
|
+
.divider {
|
|
354
|
+
border-right: 1px solid var(--border);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
button.success {
|
|
358
|
+
animation: success 1s;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@keyframes success {
|
|
363
|
+
from {
|
|
364
|
+
outline: 0px solid rgb(114, 191, 114);
|
|
365
|
+
}
|
|
366
|
+
to {
|
|
367
|
+
outline: 10px solid rgb(114, 191, 114, 0);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.saved-markers {
|
|
372
|
+
&,
|
|
373
|
+
tr,
|
|
374
|
+
td,
|
|
375
|
+
tbody {
|
|
376
|
+
margin: 0;
|
|
377
|
+
padding: 0;
|
|
378
|
+
display: block;
|
|
379
|
+
}
|
|
380
|
+
tbody {
|
|
381
|
+
width: 100%;
|
|
382
|
+
}
|
|
383
|
+
.row {
|
|
384
|
+
display: flex;
|
|
385
|
+
width: 100%;
|
|
386
|
+
border-bottom: 1px solid var(--border);
|
|
387
|
+
width: calc(100% + 1rem);
|
|
388
|
+
margin-left: -0.5rem;
|
|
389
|
+
border-radius: 1px;
|
|
390
|
+
& > * {
|
|
391
|
+
padding: 0.5rem;
|
|
392
|
+
}
|
|
393
|
+
&:not(.row--deleted):hover {
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
background: Highlight;
|
|
396
|
+
color: HighlightText;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
.row a {
|
|
400
|
+
text-decoration: none;
|
|
401
|
+
color: inherit;
|
|
402
|
+
&:focus-visible {
|
|
403
|
+
text-decoration: underline;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
.name {
|
|
407
|
+
flex: 1;
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
transition: all 0.2s;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.row--deleted .name {
|
|
414
|
+
opacity: 0.2;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
</style>
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
let {
|
|
13
13
|
defaultMarkerName = () => "Marker",
|
|
14
14
|
prefixes = {},
|
|
15
|
-
|
|
15
|
+
// Optional function to process markers
|
|
16
|
+
onMarker = (str) => str,
|
|
16
17
|
iframeUrl = "",
|
|
17
18
|
} = $props();
|
|
18
19
|
|
|
@@ -46,11 +47,8 @@
|
|
|
46
47
|
|
|
47
48
|
const uniqueMarkers = Array.from(new Set(markers));
|
|
48
49
|
|
|
49
|
-
// parse to object
|
|
50
|
-
const encodedMarkers = uniqueMarkers.map((marker) => parse(marker));
|
|
51
|
-
|
|
52
50
|
// pass through schema
|
|
53
|
-
const parsedMarkers = await Promise.all(
|
|
51
|
+
const parsedMarkers = await Promise.all(uniqueMarkers.map(onMarker));
|
|
54
52
|
|
|
55
53
|
// generate a friendly name & reencode markers with hexFlip="none"
|
|
56
54
|
preview = await Promise.all(
|
|
@@ -6,12 +6,12 @@ type ScreenshotTool = {
|
|
|
6
6
|
declare const ScreenshotTool: import("svelte").Component<{
|
|
7
7
|
defaultMarkerName?: Function;
|
|
8
8
|
prefixes?: Record<string, any>;
|
|
9
|
-
|
|
9
|
+
onMarker?: Function;
|
|
10
10
|
iframeUrl?: string;
|
|
11
11
|
}, {}, "">;
|
|
12
12
|
type $$ComponentProps = {
|
|
13
13
|
defaultMarkerName?: Function;
|
|
14
14
|
prefixes?: Record<string, any>;
|
|
15
|
-
|
|
15
|
+
onMarker?: Function;
|
|
16
16
|
iframeUrl?: string;
|
|
17
17
|
};
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
let {
|
|
11
11
|
overrideNewVersion,
|
|
12
12
|
buttonText = "Open new builder",
|
|
13
|
-
}: { overrideNewVersion
|
|
13
|
+
}: { overrideNewVersion?: NewVersion; buttonText?: string } = $props();
|
|
14
14
|
|
|
15
|
-
let newVersion = $state<NewVersion>(overrideNewVersion);
|
|
15
|
+
let newVersion = $state<NewVersion | undefined>(overrideNewVersion);
|
|
16
|
+
// svelte-ignore state_referenced_locally
|
|
16
17
|
let isOpen = $state(!!newVersion);
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -3,8 +3,8 @@ type NewVersion = {
|
|
|
3
3
|
thisVersion: string;
|
|
4
4
|
};
|
|
5
5
|
type $$ComponentProps = {
|
|
6
|
-
overrideNewVersion
|
|
7
|
-
buttonText
|
|
6
|
+
overrideNewVersion?: NewVersion;
|
|
7
|
+
buttonText?: string;
|
|
8
8
|
};
|
|
9
9
|
declare const UpdateChecker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
10
|
type UpdateChecker = ReturnType<typeof UpdateChecker>;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { default as Modal } from "./Modal/Modal.svelte";
|
|
|
5
5
|
export { default as ScreenshotTool } from "./ScreenshotTool/ScreenshotTool.svelte";
|
|
6
6
|
export { default as Typeahead } from "./Typeahead/Typeahead.svelte";
|
|
7
7
|
export { default as UpdateChecker } from "./UpdateChecker/UpdateChecker.svelte";
|
|
8
|
+
export { default as MarkerAdmin } from "./MarkerAdmin/MarkerAdmin.svelte";
|
package/dist/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { default as Modal } from "./Modal/Modal.svelte";
|
|
|
6
6
|
export { default as ScreenshotTool } from "./ScreenshotTool/ScreenshotTool.svelte";
|
|
7
7
|
export { default as Typeahead } from "./Typeahead/Typeahead.svelte";
|
|
8
8
|
export { default as UpdateChecker } from "./UpdateChecker/UpdateChecker.svelte";
|
|
9
|
+
export { default as MarkerAdmin } from "./MarkerAdmin/MarkerAdmin.svelte";
|