frontpack 0.1.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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +51 -0
  4. data/Rakefile +5 -0
  5. data/app/assets/config/frontpack_manifest.js +1 -0
  6. data/app/assets/images/logo.svg +141 -0
  7. data/app/assets/javascripts/frontpack/auto-complete.js +173 -0
  8. data/app/assets/javascripts/frontpack/base-webcomponent.js +160 -0
  9. data/app/assets/javascripts/frontpack/expandable-list.js +99 -0
  10. data/app/assets/javascripts/frontpack/image-preview.js +160 -0
  11. data/app/assets/javascripts/frontpack/image-uploader.js +34 -0
  12. data/app/assets/javascripts/frontpack/localized-time.js +17 -0
  13. data/app/assets/javascripts/frontpack/modal-dialog.js +206 -0
  14. data/app/assets/javascripts/frontpack/shortcuts.js +63 -0
  15. data/app/assets/javascripts/frontpack/tab-control.js +77 -0
  16. data/app/assets/javascripts/frontpack/text-box.js +70 -0
  17. data/app/assets/javascripts/frontpack/toggle-button.js +73 -0
  18. data/app/assets/stylesheets/frontpack/000-reset.scss +15 -0
  19. data/app/assets/stylesheets/frontpack/010-config.scss +148 -0
  20. data/app/assets/stylesheets/frontpack/020-mixins.scss +218 -0
  21. data/app/assets/stylesheets/frontpack/030-consts.scss +13 -0
  22. data/app/assets/stylesheets/frontpack/031-margins.scss +5 -0
  23. data/app/assets/stylesheets/frontpack/100-layout.scss +113 -0
  24. data/app/assets/stylesheets/frontpack/101-container.scss +131 -0
  25. data/app/assets/stylesheets/frontpack/200-design.scss +75 -0
  26. data/app/assets/stylesheets/frontpack/201-box.scss +6 -0
  27. data/app/assets/stylesheets/frontpack/202-borders.scss +5 -0
  28. data/app/assets/stylesheets/frontpack/300-typography.scss +128 -0
  29. data/app/assets/stylesheets/frontpack/800-components.scss +85 -0
  30. data/app/assets/stylesheets/frontpack/801-card.scss +53 -0
  31. data/app/assets/stylesheets/frontpack/802-buttons.scss +58 -0
  32. data/app/assets/stylesheets/frontpack/803-form-controls.scss +161 -0
  33. data/app/assets/stylesheets/frontpack/804-table.scss +60 -0
  34. data/app/assets/stylesheets/frontpack/805-switch-toggle.scss +43 -0
  35. data/app/assets/stylesheets/frontpack/frontpack.scss +18 -0
  36. data/app/controllers/concerns/frontpack/searchable.rb +33 -0
  37. data/app/controllers/frontpack/autocomplete_controller.rb +33 -0
  38. data/app/helpers/frontpack/application_helper.rb +7 -0
  39. data/app/helpers/frontpack/form_builder.rb +130 -0
  40. data/app/models/concerns/frontpack/auto_completable.rb +35 -0
  41. data/config/importmap.rb +1 -0
  42. data/config/routes.rb +3 -0
  43. data/lib/frontpack/button_to_patch.rb +64 -0
  44. data/lib/frontpack/engine.rb +28 -0
  45. data/lib/frontpack/extended_model_translations.rb +31 -0
  46. data/lib/frontpack/form_builder_options.rb +5 -0
  47. data/lib/frontpack/version.rb +5 -0
  48. data/lib/frontpack.rb +9 -0
  49. data/lib/generators/frontpack/install_generator.rb +37 -0
  50. data/lib/generators/frontpack/locale_generator.rb +11 -0
  51. data/lib/generators/templates/initializers/customize_form_with_errors.rb +31 -0
  52. data/lib/generators/templates/locales/frontpack.en.yml +43 -0
  53. data/lib/generators/templates/views/layouts/_form-errors.html.slim +3 -0
  54. data/lib/generators/templates/views/layouts/_navigation.html.slim +1 -0
  55. data/lib/generators/templates/views/layouts/application.html.slim +37 -0
  56. data/lib/generators/templates/views/layouts/errors.html.slim +19 -0
  57. data/lib/generators/templates/views/layouts/mailer.html.slim +6 -0
  58. data/lib/generators/templates/views/layouts/mailer.text.slim +1 -0
  59. data/lib/tasks/frontpack_tasks.rake +6 -0
  60. data/lib/templates/rails/file_utils/searchable.rb +30 -0
  61. data/lib/templates/rails/scaffold_controller/controller.rb.tt +93 -0
  62. data/lib/templates/slim/scaffold/_form.html.slim.tt +10 -0
  63. data/lib/templates/slim/scaffold/edit.html.slim.tt +10 -0
  64. data/lib/templates/slim/scaffold/index.html.slim.tt +20 -0
  65. data/lib/templates/slim/scaffold/new.html.slim.tt +8 -0
  66. data/lib/templates/slim/scaffold/partial.html.slim.tt +6 -0
  67. data/lib/templates/slim/scaffold/show.html.slim.tt +12 -0
  68. data/lib/templates/test_unit/model/fixtures.yml.tt +18 -0
  69. data/lib/templates/test_unit/model/unit_test.rb.tt +41 -0
  70. metadata +143 -0
@@ -0,0 +1,160 @@
1
+ class ImagePreview extends HTMLElement {
2
+ constructor () {
3
+ super()
4
+ const shadowRoot = this.attachShadow({ mode: 'open' })
5
+ shadowRoot.innerHTML = `
6
+ <style>
7
+ :host(*){
8
+ background-size: contain;
9
+ background-repeat: no-repeat;
10
+ background-position: center;
11
+ }
12
+
13
+ :host([status='empty']):before {
14
+ content: attr(text);
15
+ }
16
+ </style>
17
+ `
18
+ if (this.hasAttribute('target-input')) {
19
+ this.input = document.getElementById(this.getAttribute('target-input'))
20
+ } else {
21
+ console.log('Input file not defined')
22
+ return
23
+ }
24
+
25
+ this.setAttribute('status', 'empty')
26
+ this.source = null
27
+ }
28
+
29
+ set text (value) {
30
+ this.setAttribute('text', value)
31
+ }
32
+
33
+ get text () {
34
+ return this.getAttribute('text')
35
+ }
36
+
37
+ set status (value) {
38
+ this.setAttribute('status', value)
39
+ this._dispatch('statuschange', value)
40
+ }
41
+
42
+ get status () {
43
+ return this.getAttribute('status')
44
+ }
45
+
46
+ onClick () {
47
+ this.input.click()
48
+ }
49
+
50
+ onChange () {
51
+ this.file = this.input.files[0]
52
+ this.source = 'fileUpload'
53
+ this.render()
54
+ }
55
+
56
+ onDragOver (event) {
57
+ event.preventDefault()
58
+ this.status = 'dragging'
59
+ }
60
+
61
+ onDragLeave () {
62
+ if (!this.file) {
63
+ this.setAttribute('status', 'empty')
64
+ } else {
65
+ this.setAttribute('status', 'ready')
66
+ }
67
+ }
68
+
69
+ onDrop (event) {
70
+ event.preventDefault()
71
+ this.file = event.dataTransfer.files[0]
72
+ this.source = 'dragAndDrop'
73
+ this.render()
74
+ }
75
+
76
+ onPaste (event) {
77
+ try {
78
+ this.file = event.clipboardData.items[1].getAsFile()
79
+ this.source = 'clipboard'
80
+ this.render()
81
+ } catch (e) {
82
+ this._dispatch('error', e.message)
83
+ }
84
+ }
85
+
86
+ render () {
87
+ const validExtensions = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
88
+ if (!this.file || !validExtensions.includes(this.file.type)) {
89
+ this._dispatch('error', 'Invalid image file.')
90
+ this._clear()
91
+ return
92
+ }
93
+
94
+ try {
95
+ this.status = 'loading'
96
+ const fileReader = new FileReader()
97
+ fileReader.addEventListener('load', this.onReaderReady.bind(this))
98
+ fileReader.addEventListener('error', this.onReaderError.bind(this))
99
+ fileReader.readAsDataURL(this.file)
100
+ this.input.files = this.copyFile()
101
+ } catch (e) {
102
+ this._clear()
103
+ this._dispatch('error', e.message)
104
+ }
105
+ }
106
+
107
+ copyFile () {
108
+ const dt = new DataTransfer()
109
+ dt.items.add(new File([this.file.size, this.file.type], this.file.name))
110
+ return dt.files
111
+ }
112
+
113
+ onReaderReady (event) {
114
+ const fileURL = event.target.result
115
+ this.style.backgroundImage = `url(${fileURL})`
116
+ this.base64Data = fileURL
117
+ this.setAttribute('status', 'ready')
118
+ this.status = 'ready'
119
+ this._dispatch('success', { source: this.source })
120
+ }
121
+
122
+ onReaderError (event) {
123
+ this._dispatch('error', event.target.error)
124
+ this.status = 'error'
125
+ }
126
+
127
+ _dispatch (eventName, details) {
128
+ const event = new CustomEvent(eventName, {
129
+ cancelable: true,
130
+ bubbles: true,
131
+ detail: details
132
+ })
133
+ this.dispatchEvent(event)
134
+ }
135
+
136
+ _clear () {
137
+ this.file = null
138
+ this.style.backgroundImage = ''
139
+ this.setAttribute('status', 'empty')
140
+ this.input.files = []
141
+ }
142
+
143
+ clear () {
144
+ this._clear()
145
+ this._dispatch('clear')
146
+ }
147
+
148
+ connectedCallback () {
149
+ this.addEventListener('click', this.onClick.bind(this))
150
+ this.addEventListener('dragover', this.onDragOver.bind(this))
151
+ this.addEventListener('drop', this.onDrop.bind(this))
152
+ this.addEventListener('dragleave', this.onDragLeave.bind(this))
153
+ this.input.addEventListener('change', this.onChange.bind(this))
154
+ if (this.getAttribute('disable-clipboard') !== 'true') {
155
+ window.addEventListener('paste', this.onPaste.bind(this))
156
+ }
157
+ }
158
+ }
159
+
160
+ customElements.define('image-preview', ImagePreview)
@@ -0,0 +1,34 @@
1
+ class ImageUploader {
2
+ onUpdatePreview(event) {
3
+ this.preview.src = event.target.result;
4
+ }
5
+
6
+ onChange() {
7
+ if (this.input.files && this.input.files[0]) {
8
+ let reader = new FileReader();
9
+ reader.addEventListener('load', this.onUpdatePreview.bind(this));
10
+ reader.readAsDataURL(this.input.files[0]);
11
+ }
12
+ }
13
+
14
+ constructor(element) {
15
+ this.preview = element.querySelector('img');
16
+ this.input = element.querySelector('input');
17
+ this.input.addEventListener('change', this.onChange.bind(this));
18
+
19
+ window.setTimeout(_ => {
20
+ let previewContainer = element.querySelector('.preview');
21
+ let rect = previewContainer.getBoundingClientRect();
22
+ this.preview.style.width = rect.width + 'px';
23
+ this.preview.style.height = rect.height + 'px';
24
+ this.preview.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII= ';
25
+ }, 1);
26
+ }
27
+ }
28
+
29
+ document.addEventListener('turbolinks:load', _ => {
30
+ let uploaders = document.querySelectorAll('.image-uploader');
31
+ for (let uploader of uploaders) {
32
+ new ImageUploader(uploader);
33
+ }
34
+ });
@@ -0,0 +1,17 @@
1
+ class LocalizedTime extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ const shadowRoot = this.attachShadow({ mode: 'open' });
5
+ shadowRoot.innerHTML = `<slot></slot>`;
6
+ }
7
+
8
+ connectedCallback() {
9
+ this.shadowRoot.innerHTML = this.time.toLocaleString();
10
+ }
11
+
12
+ get time() {
13
+ let date_time = new Date(this.getAttribute('time'));
14
+ return date_time;
15
+ }
16
+ }
17
+ customElements.define('localized-time', LocalizedTime);
@@ -0,0 +1,206 @@
1
+ class ModalDialog extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' } ).innerHTML = `
5
+ <style>
6
+ * {
7
+ box-sizing: border-box;
8
+ }
9
+ :host {
10
+ position: absolute;
11
+ display: none;
12
+ flex-flow: column nowrap;
13
+ align-items: center;
14
+ justify-content: center;
15
+ left: 0;
16
+ top: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ z-index: 200;
20
+ background: rgba(128, 128, 128, 0.2);
21
+ }
22
+ #dialog {
23
+ display: grid;
24
+ grid-template-columns: 100%;
25
+ grid-template-rows: min-content 1fr min-content;
26
+ background: #f8f9fa;
27
+ box-shadow: 1px 1px 5px gray;
28
+ }
29
+ #header {
30
+ display: grid;
31
+ grid-template-columns: 1fr min-content;
32
+ grid-template-areas: "title buttons";
33
+ background: rgba(0, 0, 0, 0.1);
34
+ border-bottom: rgba(0, 0, 0, 0.2) solid 1px;
35
+ padding: 0 0.5em;
36
+ }
37
+ #title {
38
+ grid-area: title;
39
+ margin: 0;
40
+ padding: 0.25em 0;
41
+ }
42
+ #buttons {
43
+ display: inline-grid;
44
+ grid-auto-columns: 2em;
45
+ grid-template-rows: auto;
46
+ }
47
+ #title-buttons button {
48
+ font-family: "Material Icons";
49
+ font-size: 1.3em;
50
+ padding: 0;
51
+ margin: 0;
52
+ background: transparent;
53
+ border: none;
54
+ cursor: pointer;
55
+ height: 100%;
56
+ }
57
+ #title-buttons button:hover {
58
+ color: red;
59
+ }
60
+ #content {
61
+ position: relative;
62
+ padding: 0.5em;
63
+ }
64
+ #footer {
65
+ display: grid;
66
+ grid-template-columns: min-content 1fr min-content;
67
+ grid-template-rows: min-content;
68
+ grid-template-areas: "left center right";
69
+ padding: 0.5em;
70
+ }
71
+ .center-buttons {
72
+ display: flex;
73
+ flex-flow: row nowrap;
74
+ justify-content: center;
75
+ text-align: center;
76
+ }
77
+ .standard-button {
78
+ border: rgba(0, 0, 0, 0.2) 1px solid;
79
+ background: rgba(0, 0, 0, 0.1);
80
+ padding: 0.5em 1em;
81
+ }
82
+ iframe {
83
+ width: 100%;
84
+ height: 100%;
85
+ border: none;
86
+ }
87
+ </style>
88
+ <div id="dialog">
89
+ <div id="header">
90
+ <h3 id="title"></h3>
91
+ <span id="title-buttons">
92
+ <button type="button" id="close-button" data-resolve="false" data-data="cancel">close</button>
93
+ </span>
94
+ </div>
95
+ <div id="content">
96
+ <slot></slot>
97
+ </div>
98
+ <div id="footer">
99
+ <div class="left-buttons"><slot name="left"></slot></div>
100
+ <div class="center-buttons"><slot name="center"></slot></div>
101
+ <div class="right-buttons"><slot name="right"></slot></div>
102
+ </div>
103
+ </div>
104
+ `;
105
+ }
106
+
107
+ connectedCallback() {
108
+ this._title = this.shadowRoot.getElementById('title');
109
+ this._content = this.shadowRoot.getElementById('content');
110
+ this._buttons = new Map([
111
+ ['left', this.shadowRoot.getElementById('left')],
112
+ ['center', this.shadowRoot.getElementById('center')],
113
+ ['right', this.shadowRoot.getElementById('right')]
114
+ ]);
115
+ this._footer = this.shadowRoot.getElementById('footer');
116
+ this._footer.addEventListener('click', this.onButtonClick.bind(this));
117
+ this._close_button = this.shadowRoot.getElementById('close-button');
118
+ this._close_button.addEventListener('click', this.onButtonClick.bind(this));
119
+ this._title.innerHTML = this.caption;
120
+ }
121
+
122
+ get caption() {
123
+ return this.getAttribute('caption');
124
+ }
125
+
126
+ set caption(value) {
127
+ this.setAttribute('caption', value);
128
+ this._title.innerHTML = value;
129
+ }
130
+
131
+ get closeButton() {
132
+ return this._close_button.style.display = 'inline';
133
+ }
134
+
135
+ set closeButton(value) {
136
+ this._close_button.style.display = value ? 'inline' : 'none';
137
+ }
138
+
139
+ set buttons(options) {
140
+ ['left', 'center', 'right'].forEach((position) => {
141
+ this._buttons.get(position).innerHTML = '';
142
+ });
143
+ for (let button of options) {
144
+ let newButton = document.createElement('button');
145
+ newButton.innerHTML = button.caption;
146
+ newButton.dataset.data = button.data;
147
+ newButton.dataset.resolve = button.resolve;
148
+ newButton.className = 'standard-button';
149
+ this._buttons.get(button.position).appendChild(newButton);
150
+ }
151
+ }
152
+
153
+ onButtonClick(event) {
154
+ let button = event.target;
155
+ if (button.tagName.toLowerCase() !== 'button') {
156
+ return;
157
+ }
158
+
159
+ this.close(button.dataset.resolve === 'true', button.dataset.data);
160
+ }
161
+
162
+ show(options = {}) {
163
+ return new Promise((resolve, reject) => {
164
+ if (options.hideCloseButton) {
165
+ this.closeButton = false;
166
+ }
167
+ if (options.width) {
168
+ this._content.style.width = options.width;
169
+ }
170
+ if (options.height) {
171
+ this._content.style.height = options.height;
172
+ }
173
+ this.resolve = resolve;
174
+ this.reject = reject;
175
+ this.style.display = 'inline-flex';
176
+ });
177
+ }
178
+
179
+ dialogBox(caption, text, buttons, options = {}) {
180
+ this.caption = caption;
181
+ this._content.innerHTML = `<p>${text}</p>`;
182
+ this.buttons = buttons;
183
+ return this.show(options);
184
+ }
185
+
186
+ messageBox(caption, text, options = {}) {
187
+ return this.dialogBox(caption, text, [{ caption: '✔', data: 'ok', resolve: true, position: 'center' }], options);
188
+ }
189
+
190
+ confirmationBox(caption, text, options = {}) {
191
+ return this.dialogBox(caption, text, [
192
+ { caption: '✔', data: 'yes', resolve: true, position: 'left' },
193
+ { caption: '❌', data: 'no', resolve: true, position: 'right' }
194
+ ], options);
195
+ }
196
+
197
+ close(resolve, data = '') {
198
+ this.style.display = 'none';
199
+ if (resolve) {
200
+ this.resolve(data);
201
+ } else {
202
+ this.reject(data);
203
+ }
204
+ }
205
+ }
206
+ customElements.define('modal-dialog', ModalDialog);
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ window.Shortcut = {
4
+ _registeredActions: new Map(),
5
+ _registeredShortcuts: new Map(),
6
+ _actionNameIndex: 0,
7
+
8
+ _parseShortcut(shortcut) {
9
+ if (!/^(?:(ctrl|alt|shift)\+){0,3}[a-z0-9]+$/i.test(shortcut)) {
10
+ throw new Error('Invalid shortcut combination');
11
+ }
12
+ },
13
+
14
+ onKeyUp(event) {
15
+ let { ctrlKey, shiftKey, altKey, code } = event;
16
+ let tokens = [];
17
+ if (ctrlKey) { tokens.push('ctrl'); }
18
+ if (altKey) { tokens.push('alt'); }
19
+ if (shiftKey) { tokens.push('shift'); }
20
+ tokens.push(code.toLowerCase().replace(/(key|digit)/, ''));
21
+ let shortcut = tokens.join('+').toLowerCase();
22
+
23
+ if (this._registeredShortcuts.has(shortcut)) {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ this.execute(this._registeredShortcuts.get(shortcut));
27
+ }
28
+ },
29
+
30
+ nextActionName() {
31
+ this._actionNameIndex++;
32
+ return `Action${this._actionNameIndex}`;
33
+ },
34
+
35
+ execute(name) {
36
+ this._registeredActions.get(name)();
37
+ },
38
+
39
+ /**
40
+ * Register a named action with an optional shortcut
41
+ * @param {String} name Action name
42
+ * @param {CallableFunction} callback Action to be executed
43
+ * @param {String} shortcut Any shortcut to listen globally
44
+ */
45
+ registerAction(name, callback, shortcut = null) {
46
+ this._registeredActions.set(name, callback);
47
+ if (shortcut) {
48
+ this._parseShortcut(shortcut.toLowerCase());
49
+ this._registeredShortcuts.set(shortcut.toLowerCase(), name);
50
+ }
51
+ },
52
+
53
+ start() {
54
+ document.addEventListener('keydown', this.onKeyUp.bind(this));
55
+ let elements = document.querySelectorAll('[shortcut]');
56
+ for (let element of elements) {
57
+ let actionName = this.nextActionName();
58
+ this.registerAction(actionName, element.click.bind(element), element.getAttribute('shortcut'));
59
+ }
60
+ }
61
+ };
62
+
63
+ export default Shortcut;
@@ -0,0 +1,77 @@
1
+ class TabNav extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ const shadowRoot = this.attachShadow({ mode: 'open' });
5
+ shadowRoot.innerHTML = `<slot></slot>`;
6
+ this.currentTab = null;
7
+ }
8
+
9
+ connectedCallback() {
10
+ this.addEventListener('tabshow', this.onTabClick.bind(this));
11
+ }
12
+
13
+ onTabClick(event) {
14
+ if (this.currentTab) {
15
+ this.currentTab.active = false;
16
+ }
17
+ this.currentTab = event.detail.tab;
18
+ this.currentTab.active = true;
19
+ }
20
+ }
21
+ customElements.define('tab-nav', TabNav);
22
+
23
+ class TabButton extends HTMLElement {
24
+ constructor() {
25
+ super();
26
+ const shadowRoot = this.attachShadow({ mode: 'open' });
27
+ shadowRoot.innerHTML = `<slot></slot>`;
28
+ }
29
+
30
+ connectedCallback() {
31
+ this.addEventListener('click', this.onClick.bind(this));
32
+ this._target = document.getElementById(this.target);
33
+ if (this.hasAttribute('active')) {
34
+ this.active = true;
35
+ this.dispatchClickEvent();
36
+ } else {
37
+ this.active = false;
38
+ }
39
+ }
40
+
41
+ get target() {
42
+ return this.getAttribute('target');
43
+ }
44
+
45
+ set target(value) {
46
+ this.setAttribute('target', value);
47
+ this._target = document.getElementById(value);
48
+ }
49
+
50
+ get active() {
51
+ return this.hasAttribute('active');
52
+ }
53
+
54
+ set active(value) {
55
+ if (value) {
56
+ this.setAttribute('active', 'true');
57
+ this._target.style.zIndex = '10';
58
+ } else {
59
+ this.removeAttribute('active');
60
+ this._target.style.zIndex = '0';
61
+ }
62
+ }
63
+
64
+ onClick() {
65
+ if (this.active) {
66
+ return;
67
+ }
68
+
69
+ this.dispatchClickEvent();
70
+ }
71
+
72
+ dispatchClickEvent() {
73
+ let event = new CustomEvent('tabshow', { cancelable: true, bubbles: true, detail: { tab: this } });
74
+ this.dispatchEvent(event);
75
+ }
76
+ }
77
+ customElements.define('tab-button', TabButton);
@@ -0,0 +1,70 @@
1
+ class TextBox extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ const shadowRoot = this.attachShadow({ mode: 'open' });
5
+ shadowRoot.innerHTML = `
6
+ <style>
7
+ :host {
8
+ display: none;
9
+ grid-template-columns: min-content 1fr min-content;
10
+ grid-template-rows: auto;
11
+ align-items: center;
12
+ align-content: center;
13
+ grid-gap: .5rem;
14
+ padding: .5rem;
15
+ }
16
+
17
+ i, button {
18
+ border: none;
19
+ background: none;
20
+ font-family: "Material Icons";
21
+ font-style: normal;
22
+ font-size: 1.2rem;
23
+ color: inherit;
24
+ }
25
+ </style>
26
+ <i></i>
27
+ <div><slot></slot></div>
28
+ <button type="button">close</button>
29
+ `;
30
+ }
31
+
32
+ connectedCallback() {
33
+ this._icon = this.shadowRoot.querySelector('i');
34
+ this._icon.innerText = this.icon;
35
+ this._closeButton = this.shadowRoot.querySelector('button');
36
+ this._closeButton.addEventListener('click', this.close.bind(this));
37
+ this.persistent = this.hasAttribute('persistent');
38
+
39
+ if (this.persistent && this.hasAttribute('id')) {
40
+ let key = `${document.location.pathname.substring(1)}#${this.id}`;
41
+ if (localStorage.getItem(key) === 'true') {
42
+ return;
43
+ }
44
+ }
45
+ this.open();
46
+ }
47
+
48
+ get icon() {
49
+ return this.getAttribute('icon');
50
+ }
51
+
52
+ set icon(value) {
53
+ this.setAttribute('icon', value);
54
+ this._icon.innerText = value;
55
+ }
56
+
57
+ open() {
58
+ this.style.display = 'grid';
59
+ }
60
+
61
+ close() {
62
+ this.style.display = 'none';
63
+
64
+ if (this.persistent && this.hasAttribute('id')) {
65
+ let key = `${document.location.pathname.substring(1)}#${this.id}`;
66
+ localStorage.setItem(key, 'true');
67
+ }
68
+ }
69
+ }
70
+ customElements.define('text-box', TextBox);
@@ -0,0 +1,73 @@
1
+ class ToggleButton extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ const shadowRoot = this.attachShadow({mode: 'open'});
5
+ shadowRoot.innerHTML = '<slot></slot>';
6
+ }
7
+
8
+ connectedCallback() {
9
+ this.mode = this.getAttribute('mode');
10
+
11
+ if (this.mode === 'copy') {
12
+ this.addEventListener('click', this.copy.bind(this));
13
+ } else {
14
+ this.addEventListener('click', this.toggle.bind(this));
15
+ }
16
+ }
17
+
18
+ target() {
19
+ if (!this._target) {
20
+ this._target = document.getElementById(this.getAttribute('target'));
21
+ }
22
+
23
+ return this._target;
24
+ }
25
+
26
+ render() {
27
+ if (this.state) {
28
+ this.target().classList.remove(this.hiddenClass)
29
+ this.target().classList.add(this.visibleClass)
30
+ } else {
31
+ this.target().classList.remove(this.visibleClass)
32
+ this.target().classList.add(this.hiddenClass)
33
+ }
34
+ }
35
+
36
+ copy () {
37
+ const target = this.target()
38
+
39
+ if (typeof target.value !== 'undefined') {
40
+ navigator.clipboard.writeText(target.value)
41
+ } else {
42
+ navigator.clipboard.writeText(target.innerText)
43
+ }
44
+ }
45
+
46
+ toggle() {
47
+ this.state = !this.state;
48
+ }
49
+
50
+ get state() {
51
+ return this.hasAttribute('on');
52
+ }
53
+
54
+ set state(value) {
55
+ if (value) {
56
+ this.setAttribute('on', value);
57
+ } else {
58
+ this.removeAttribute('on');
59
+ }
60
+
61
+ this.render();
62
+ }
63
+
64
+ get visibleClass() {
65
+ return this.getAttribute('visible-class');
66
+ }
67
+
68
+ get hiddenClass() {
69
+ return this.getAttribute('hidden-class');
70
+ }
71
+ }
72
+
73
+ customElements.define('toggle-button', ToggleButton);