Budujemy rozszerzenie do Chrome - #2 (Panel ustawień)

Wpis jest drugim z cyklu “Budujemy rozszerzenie do Chrome”. W tym kroku zabierzemy się za zbudowanie strony ustawień, na której zdefiniujemy tło strony nowej karty i zdefiniujemy widgety z kategoriami zakładek.

  1. Budujemy rozszerzenie do Chrome - #1 (Plik manifestu)
  2. Budujemy rozszerzenie do Chrome - #2 (Panel ustawień)
  3. Budujemy rozszerzenie do Chrome - #3 (Strona frontowa)
  4. Budujemy rozszerzenie do Chrome - #4 (Wielojęzyczność i publikacja w Chrome Store)

Cała dzisiejsza praca będzie się skupiać wokół dwóch plików (options.html i options.js). Pominę arkusze styli, znajdują się w repozytorium na Github.

Kod HTML

Strona ustawień będzie podzielona na pół w pionie. Lewą stronę wypełni formularz edycji ustawień, po prawej wybór widgetów, które dodamy do strony startowej.

Plik options.html

1
2
3
4
5
6
<div class="container">
    <div class="row">
        <div class="col" id="settings"></div>
        <div class="col two cards" id="widgets"></div>
    </div>
</div>

Kod JavaScript

Plik options.js

1
2
3
4
5
6
7
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
let storage = {};
let widgets = [];
let constants = {
  backgroundDir: 'images/background/',
};

class Helpers {
  static getImageBase64(url) {
    return new Promise((resolve, reject) => {
      var xhr = new XMLHttpRequest();
      xhr.onload = function() {
        var reader = new FileReader();
        reader.onloadend = function() {
          resolve(reader.result);
        };
        reader.readAsDataURL(xhr.response);
      };
      xhr.open('GET', url);
      xhr.responseType = 'blob';
      xhr.send();
    });
  }
}

class App {
  static widgetSave(widget) {
    chrome.storage.sync.get(function(storage) {
      if (!chrome.runtime.error) {
        let id = 0;

        if (!storage.widgets) {
          storage.widgets = [];
        } else {
          storage.widgets.forEach(function (item) {
            if (item.id > id) {
              id = item.id;
            }
          })
        }
        ++id;

        widget = {...widget, ...{id}};
        storage.widgets.push(widget);

        App.storageSave(storage);
      }
    });
  }

  static settingsSave(settings) {
    storage.settings = settings;
    App.storageSave(storage);
  }

  static storageSave(storage) {
    chrome.storage.sync.set(storage, function() {
      Message.success(chrome.i18n.getMessage('settings_saved'));
    });
  }
}

class Settings {
  constructor() {

  }

  draw() {
    let backgroundUrl = '';
    if (storage.settings.background.substring(0, 4) === 'http') {
      backgroundUrl = storage.settings.background;
    }

    let html = `
      <div class="header">
        ${chrome.i18n.getMessage('widget_settings_title')}
      </div>
      <div class="body">
        <form class="form">
          <div class="field">
            <label>${chrome.i18n.getMessage('widget_settings_field_columns_label')}</label>
            <input type="number" id="settings_columns" value="${storage.settings.columns}">
          </div>
          <div class="field">
            <label>${chrome.i18n.getMessage('widget_settings_field_backgroundGallery_label')}</label>
            <div class="gallery" id="background-gallery"></div>
          </div>
          <div class="field">
            <label>${chrome.i18n.getMessage('widget_settings_field_backgroundUrl_label')}</label>
            <input type="text" id="settings_background_url" value="${backgroundUrl}">
            <span class="help">${chrome.i18n.getMessage('widget_settings_field_backgroundUrl_help')}</span>
          </div>
          <input type="hidden" id="settings_background" value="${storage.settings.background}">
        </form>
      </div>
      <div class="footer">
        <button type="button" class="button blue" id="settings_submit">${chrome.i18n.getMessage('widget_settings_submit')}</button>
      </div>
    `;

    let container = document.createElement('div');
    container.setAttribute('class', 'card border-0 shadow-0');
    container.innerHTML = html;

    document.getElementById('settings').appendChild(container);

    this.drawBackgroundGallery();

    document.getElementById('settings_background_url').addEventListener('change', this.setBackgroundFromURL);

    document.getElementById('settings_submit').addEventListener('click', this.save);
  }

  drawBackgroundGallery() {
    let images = [
      '1.jpg',
      '2.jpg',
      '3.jpg',
      '4.jpg',
      '5.jpg'
    ];

    images.forEach(image => {
      let imageElement = document.createElement('img');
      let imageUrl = chrome.runtime.getURL(constants.backgroundDir + image);
      imageElement.setAttribute('class', 'image');
      imageElement.setAttribute('src', imageUrl);
      imageElement.setAttribute('data-background-id', image);

      if (storage.settings.background === image) {
        imageElement.classList.add('selected');
      }

      imageElement.addEventListener('click', this.setBackgroundFromGallery);

      document.getElementById('background-gallery').appendChild(imageElement);
    });
  }

  setBackgroundFromGallery(event) {
    document.getElementById('settings_background').value = event.target.getAttribute('data-background-id');
    document.getElementById('settings_background_url').value = '';

    document.querySelectorAll('#background-gallery .image').forEach(element => {
      element.classList.remove('selected');
    });
    event.target.classList.add('selected');
  }

  setBackgroundFromURL(event) {
    document.getElementById('settings_background').value = event.target.value;
    document.querySelectorAll('#background-gallery .image').forEach(element => {
      element.classList.remove('selected');
    });
  }

  save() {
    let settings = {
      columns: parseInt(document.getElementById('settings_columns').value),
      background: document.getElementById('settings_background').value,
    };

    if (storage.settings.backgroundUrl !== settings.backgroundUrl && settings.backgroundUrl.length > 0) {
      Helpers.getImageBase64(settings.backgroundUrl)
        .then(result => {
          localStorage.setItem('backgroundBase64', result);

        });
    }

    App.settingsSave(settings);
  }
}

/**
 * Bookmarks Widget
 * @type {widgetBookmarks}
 */
let widgetBookmarksClass = class widgetBookmarks {
  constructor() {

  }

  draw() {
    let html = `
    <div class="header">
      <h2>${chrome.i18n.getMessage('widget_bookmarks_title')}</h2>
    </div>
    <form class="form" id="widget_bookmarks">
    <div class="body">
      <div class="field">
        <label>${chrome.i18n.getMessage('widget_bookmarks_field_title_label')}</label>
        <input type="text" id="widget_bookmarks_title">
      </div>
      <div class="field">
        <label>${chrome.i18n.getMessage('widget_bookmarks_field_group_label')}</label>
        <select id="widget_bookmarks_group"></select>
      </div>
    </div>
    <div class="footer">
      <button type="button" class="button blue" id="widget_bookmarks_submit">${chrome.i18n.getMessage('widget_add')}</button>
    </div>
    </form>
    `;

    let container = document.createElement('div');
    container.setAttribute('id', 'widget-bookmarks');
    container.setAttribute('class', 'card widget bookmarks');
    container.innerHTML = html;

    document.getElementById('widgets').appendChild(container);

    document.getElementById('widget_bookmarks_group').addEventListener('change', function (event) {
      document.getElementById('widget_bookmarks_title').setAttribute('value', event.target.options[event.target.selectedIndex].text);
    });
    document.getElementById('widget_bookmarks_submit').addEventListener('click', this.save);
    this.drawGroups();
  }

  drawGroups() {
    this.getGroups().then(result => {
      let groups = this.drawGroupsOptions(result[0].children);
      document.querySelector('#widget-bookmarks select').innerHTML = groups;
    });
  }

  drawGroupsOptions(bookmarkNodes) {
    let html = '';
    let i;
    for (i = 0; i < bookmarkNodes.length; i++) {
      if (bookmarkNodes[i].children && bookmarkNodes[i].children.length > 0) {
        html += `<option value="${bookmarkNodes[i].id}">${bookmarkNodes[i].title}</option>`;
        html += this.drawGroupsOptions(bookmarkNodes[i].children);
      }
    }
    return html;
  }

  getGroups() {
    return new Promise((resolve, reject) => {
      chrome.bookmarks.getTree(
        function(result) {
          resolve(result);
        });
    });
  }

  save() {
    let title = document.getElementById('widget_bookmarks_title').value;
    let group = document.getElementById('widget_bookmarks_group').value;

    let widget = {
      column: 1,
      type: 'bookmarks',
      title,
      group
    };
    App.widgetSave(widget);
  }
};
widgets.push(widgetBookmarksClass);

document.body.onload = function() {
  chrome.storage.sync.get(function(result) {
    if (!chrome.runtime.error) {
      storage = result;

      new Settings().draw();
      widgets.forEach(function (widget) {
        new widget().draw();
      });
    }
  });
};

Powyższy kod możemy podzielić na trzy kategorie:

  • narzędzia globalne
  • edycja ustawień
  • dodawanie widgetów

Narzędzia globalne

Znajdują się w liniach 1-60 oraz 262-273.

1-5 - definiujemy globalne zmienne, z których będziemy korzystać. Zmienna storage zostanie wypełniona zaraz po wyzwoleniu eventu onload na body. Event jest zadeklarowany w linii 265.

7-23 - funkcje wspomagające. Na tą chwilę jest jedna, która będzie przydatna dla edycji ustawień. Potem do niej nawiążę.

25-60 - podstawowe funkcje. Na tą chwilę są to trzy metody do zapisu danych w storage. widgetSave służy do dodania widgetu do storage. settingsSave do zapisu ustawień, storageSave zapisuje dane w storage. Dwie poprzednie metody używają storageSave do zapisu danych w storage.

W poprzedniej części wspominałem o dwóch metodach przechowywania danych w storage (lokalnie i w chmurze). W powyższym kodzie wykorzystuję zapis do chmury. Definiuje go odwołanie się do chrome.storage.sync. Gdybyśmy używali lokalnego storage, użylibyśmy chrome.storage.local.

262-273 - document.body.onload to event, który wyzwoli się w momencie, gdy cala zawartość strony ustawień, wraz ze skryptami i stylami, zostanie zaczytana. W pierwszej kolejności pobieramy całą zawartość storage przeglądarki i zapisujemy go do zmiennej storage. Następnie inicjowane są klasy do wyrysowania w drzewie DOM zawartości formularza ustawień i dodawania widgetów.

Edycja ustawień

Zawarta jest w klasie Settings.

Zaraz po załadowaniu się strony, uruchamiana jest metoda draw, której zadaniem jest narysowanie widgetu ustawień.

Mamy do dyspozycji dwie metody ustalania tła. Wybór z galerii lub wybór z URL.

W przypadku tej drugiej metody, postanowiłem nieco utrudnić sobie życie i zapisywać lokalnie zdjęcia z URL. Nie obyło się jednak bez pewnych problemów.

W pierwszej kolejności planowałem zastosować wbudowane funkcje Chrome do obsługi pobierania plików (chrome.downloads) i po prostu pobrałem zdjęcie na dysk. Chrome ma szczegółowe informacje o lokalizacji zdjęcia. Niestety, nie byłem w stanie ustawić tych zdjęć jako tła w css jako URL. Chrome ze względów bezpieczeństwa, nie pozwala przez API wgrywać zdjęć w inne miejsca niż domyślny folder pobierania, więc musiałem szukać innej metody.

Wybrałem przechowywanie tła w Chrome Storage w postaci base64. Sprawdzona i pewna metoda, ale ma jeden mankament - wielkość danych zazwyczaj przekracza dopuszczalne limity danych, jakie możemy przechować jako wartość pojedynczej zmiennej w chrome storage.

Ostatecznie więc zdecydowałem się jako storage, użyć localStorage. Jest to jedyna dana, która przechowuje swoje dane w innym miejscu niż storage wbudowany w Chrome.

Dodawanie widgetów

Obsługuje go klasa widgetBookmarks.

Podobnie jak w klasie Settings, w pierwszej kolejności wywoływana jest metoda draw.

Kluczowe w niej są metody związane z rysowaniem drzewa kategorii zakładek. W pierwszej kolejności wywoływana jest metoda drawGroups. Wewnątrz niej znajduje się callback do metody getGroups, która pobiera z Chrome zakładki w formie drzewa. Robi to metoda chrome.bookmarks.getTree.

Następnie drawGroups iteruje po elementach root drzewa i wywołuje na nich rekurencyjną funkcję drawGroupsOptions.

Potem już tylko save i po sprawie.

Podsumowanie

Na dziś kończymy. Mamy gotową stronę edycji ustawień. Wygląda tak:

Podgląd

W następnym artykule przygotujemy prezentację tego, cośmy ustawili i gotowe rozszerzenie zainstalujemy sobie do celów developerskich w Chrome.

Całość kodu rozszerzenia dostępna jest na GitHub.