RSS

Lepsze wczytywanie plików tekstowych do aplikacji JavaScript

Liczba odsłon: 130

Przed mie­sią­cem po­ka­za­łem jak moż­na wczy­tać plik tek­sto­wy do apli­kac­ji Java­Script bez ko­niecz­noś­ci an­ga­żo­wa­nia ser­we­ra do te­go za­da­nia. Zaprezen­to­wa­ne roz­wią­za­nie by­ło jed­nak ma­ło ele­ganc­kie. Dzisiejszy ar­ty­kuł za­wie­ra kod two­rzą­cy bar­dziej atrak­cyj­ny wi­zu­al­nie i łat­wiej­szy do opro­gra­mo­wa­nia ele­ment gra­ficz­ny.

Przypomnij­my, ja­kie by­ły nie­do­sko­na­ło­ści po­przed­nio za­pre­zen­to­wa­ne­go roz­wią­za­nia:

Najpierw roz­pra­wi­my się z wy­glą­dem ele­men­tu. Okazuje się, że naj­lep­szym spo­so­bem na je­go zmia­nę jest… ukry­cie go i za­stą­pie­nie zwyk­łym przy­ci­skiem. Niestety, ta­ki przy­cisk nie po­zwo­li nam wska­zać pli­ku i nie wczy­ta go. Rozwiąza­nie te­go prob­le­mu jest nie­zmier­nie cie­ka­we: na­le­ży na­ło­żyć sztucz­nie po­więk­szo­ny ele­ment ste­ru­ją­cy wy­bo­ru pli­ku na nasz przy­cisk, po czym uczy­nić go nie ty­le nie­wi­docz­nym, co cał­ko­wi­cie przez­ro­czy­stym. Widać bę­dzie le­żą­cy po­ni­żej przy­cisk, jed­nak na kli­ka­nie i prze­cią­ga­nie ikon bę­dzie rea­go­wał umiesz­czo­ny po­wy­żej – choć przez­ro­czy­sty – ele­ment <in­put type="file">.

Zacznijmy od za­pi­sa­nia for­mu­la­rza, w któ­rym bę­dzie­my chcie­li za­wrzeć ogra­ni­czo­ną funkcjo­nal­ność na­szej apli­kac­ji.

<form id="form">
 <textarea id="editor"></textarea>
</form>

W tym frag­men­cie ko­du stro­ny nie wy­stę­pu­je nic, co by od­po­wia­da­ło ele­men­to­wi wczy­ty­wa­nia pli­ku. Będziemy go ge­ne­ro­wać dyna­micz­nie w spo­sób, któ­ry za­pew­ni je­go sa­tys­fak­cjo­nu­ją­cy wy­gląd i dzia­ła­nie. Zaczniemy jed­nak od naj­prost­szej, nie­ele­ganc­kiej po­sta­ci. Dzięki te­mu bę­dzie­my mog­li roz­bu­do­wy­wać przy­kład ma­jąc pew­ność, że po­przed­nia wer­sja dzia­ła po­praw­nie.

function FileUploadControl(config) {
   var fileInput = document.createElement('input');
   fileInput.type = 'file';
   fileInput.id = config.id;
   return fileInput;
}

Jak moż­na się do­my­ślać, pa­ra­metr con­fig to re­fe­ren­cja do obiek­tu prze­cho­wu­ją­ce­go kon­fi­gu­rac­ję two­rzo­ne­go kom­po­nen­tu. Jedynym na ra­zie ob­słu­gi­wa­nym po­lem te­go obiek­tu jest id, w któ­rym mo­że­my po­dać uni­ka­to­wy iden­ty­fi­ka­tor ele­men­tu stro­ny.

Aby stwo­rzyć kom­po­nent i do­dać go do for­mu­la­rza, wy­star­czy utwo­rzyć obiekt zgod­ny z pro­to­ty­pem FileUploadControl i do­dać go do for­mu­la­rza form.

var uploadControl = new FileUploadControl({
   id: 'upload'
});
document.getElementById('form').appendChild(uploadControl);

Poniżej znaj­du­je się do­kład­nie ten frag­ment stro­ny w for­mie po­zwa­la­ją­cej na prze­tes­to­wa­nie go. Oczywiś­cie, na ra­zie kom­po­nent w ogó­le nie re­a­gu­je na wska­zy­wa­nie pli­ku do za­ła­do­wa­nia.

W na­stęp­nym kro­ku za­sto­su­je­my sztucz­kę przed­sta­wio­ną we wspom­nia­nym ar­ty­ku­le i za­stą­pi­my stan­dar­do­wy ele­ment stro­ny włas­nym przy­ci­skiem. Aby to uczy­nić, wy­star­czy za­mie­nić kon­struk­tor FileUploadControl no­wą wer­sją, two­rzą­cą oka­la­ją­cy ele­ment <div>, we­wnętrz­ny przy­cisk <button> oraz przy­kry­wa­ją­cy go, lecz przez­ro­czy­sty ele­ment wczy­ty­wa­nia pli­ku.

function FileUploadControl(config) {
   config = config || {};
   var wrapper = document.createElement('div');
   wrapper.id = config.id;
   wrapper.style.position = 'relative';
   wrapper.style.overflow = 'hidden';
   wrapper.style.display = 'inline-block';
   var button = document.createElement('button');
   button.className = config.buttonClassName;
   button.innerHTML = config.buttonLabel ? config.buttonLabel : 'Przeglądaj...';
   var fileInput = document.createElement('input');
   fileInput.type = 'file';
   fileInput.id = config.id ? config.id + '_input' : undefined;
   fileInput.style.fontSize = '100px';
   fileInput.style.position = 'absolute';
   fileInput.style.left = 0;
   fileInput.style.top = 0;
   fileInput.style.opacity = 0;
   wrapper.appendChild(button);
   wrapper.appendChild(fileInput);
   return wrapper;
}

Powyższy kod Java­Script jest ob­szer­ny, lecz ma­ło skom­pli­ko­wa­ny. Aby unik­nąć za­leż­noś­ci od ze­wnętrz­ne­go ar­ku­sza sty­lu, kon­struk­tor na­rzu­ca nie­zbęd­ne de­fi­ni­cje sty­lu wprost na two­rzo­ne ele­men­ty. Większą ele­gan­cję ko­du – ale też więk­szą po­dat­ność na błę­dy kon­fi­gu­ra­cyj­ne – moż­na by uzys­kać wy­dzie­la­jąc ba­zo­wy styl każ­de­go z trzech two­rzo­nych ele­men­tów do ar­ku­sza sty­lu.

Łatwo za­uwa­żyć, że obiekt kon­fi­gu­ra­cyj­ny con­fig mo­że za­wie­rać te­raz du­żo wię­cej pól. Tak jak po­przed­nio, id okre­śla uni­ka­to­wy iden­ty­fi­ka­tor ele­men­tu — jed­nak nie te­go stan­dar­do­we­go, lecz oka­la­ją­ce­go <div>. Pole buttonClassName umoż­li­wia na­rzu­ce­nie włas­nej kla­sy sty­lu na przy­cisk wy­bo­ru pli­ku, dzię­ki cze­mu moż­na ste­ro­wać je­go wy­glą­dem, a buttonLabel po­zwa­la wpro­wa­dzić do­wol­ny tekst lub ob­raz do wnęt­rza przy­ci­sku.

Poniżej znaj­du­je się do­kład­nie ten frag­ment stro­ny w for­mie po­zwa­la­ją­cej na prze­tes­to­wa­nie go. Nadal nie da się wczy­tać za­war­toś­ci pli­ku, lecz za­miast stan­dar­do­we­go ele­men­tu na klik­nię­cia (i prze­ciąg­nię­cia ikon) re­a­gu­je zwyk­ły przy­cisk.

W ko­lej­nym kro­ku mu­si­my przy­wró­cić moż­li­wość re­ago­wa­nia na po­stęp wczy­ty­wa­nia pli­ku oraz osta­tecz­nie na je­go za­ła­do­wa­nie. Załóżmy przy tym, że obiekt kon­fi­gu­ra­cyj­ny con­fig bę­dzie za­wie­rał czte­ry op­cjo­nal­ne re­fe­ren­cje do funk­cji reagu­ją­cych na zda­rze­nia:

Konstruk­tor two­rzą­cy nasz kom­po­nent uleg­nie w związ­ku z tym roz­bu­do­wie o frag­men­ty za­pre­zen­to­wa­ne w po­przed­nim ar­ty­ku­le, od­po­wie­dzial­ne za re­ago­wa­nie na ge­ne­ro­wa­ne przez prze­glą­dar­kę WWW zda­rze­nia zwią­za­ne z wczy­ty­wa­niem pli­ku.

function FileUploadControl(config) {
   config = config || {};
   var wrapper = document.createElement('div');
   wrapper.id = config.id;
   wrapper.style.position = 'relative';
   wrapper.style.overflow = 'hidden';
   wrapper.style.display = 'inline-block';
   var button = document.createElement('button');
   button.className = config.buttonClassName;
   button.innerHTML = config.buttonLabel ? config.buttonLabel : 'Przeglądaj...';
   var fileInput = document.createElement('input');
   fileInput.type = 'file';
   fileInput.id = config.id ? config.id + '_input' : undefined;
   fileInput.style.fontSize = '100px';
   fileInput.style.position = 'absolute';
   fileInput.style.left = 0;
   fileInput.style.top = 0;
   fileInput.style.opacity = 0;
   fileInput.onchange = function() {
      if (config.onstart) config.onstart.call(this);
      var reader = new FileReader();
      reader.readAsText(fileInput.files[0], 'UTF-8');
      reader.onprogress = function(evt) {
         if (config.onprogress) config.onprogress.call(this, {
            loaded: evt.loaded,
            total: evt.total
         });
      };
      reader.onload = function(evt) {
         if (config.onload) config.onload.call(this, {
            contents: evt.target.result
         });
      };
      reader.onerror = function(evt) {
         if (config.onerror) config.onerror.call(this);
      };
   };
   wrapper.appendChild(button);
   wrapper.appendChild(fileInput);
   return wrapper;
}

Oczywiś­cie, że­by wy­ko­rzys­tać moż­li­woś­ci da­wa­ne przez kom­po­nent, na­le­ży zde­fi­nio­wać co naj­mniej funk­cję re­agu­ją­cą na po­ja­wie­nie się treś­ci. W po­rów­na­niu do roz­wią­za­nia sprzed mie­sią­ca, kod za­pew­nia­ją­cy to w tym przy­pad­ku jest za­ska­ku­ją­co zwięz­ły.

var uploadControl = new FileUploadControl({
   id: 'upload',
   onload: function(file) {
      document.getElementById('editor').innerHTML = file.contents;
   },
   onerror: function(file) {
      alert('Błąd wczytywania zawartości pliku.');
   }
});
document.getElementById('form').appendChild(uploadControl);

Poniżej moż­na po­ob­ser­wo­wać tę wer­sję ko­du w dzia­ła­niu.

Do roz­wią­za­nia zo­sta­ła już tyl­ko jed­na kwe­stia. Miłoby by by­ło, gdy­by by­ło moż­li­we od­rzu­ce­nie pli­ków o nie­właś­ci­wym ty­pie za­war­toś­ci tak, aby ja­ko pli­ku tek­sto­wy nie zo­stał wczy­ta­ny na przy­kład ob­raz JPEG. Zdefiniujmy za­tem ko­lej­ne dwa po­la obiek­tu kon­fi­gu­ra­cyj­ne­go con­fig:

function FileUploadControl(config) {
   config = config || {};
   var wrapper = document.createElement('div');
   wrapper.id = config.id;
   wrapper.style.position = 'relative';
   wrapper.style.overflow = 'hidden';
   wrapper.style.display = 'inline-block';
   var button = document.createElement('button');
   button.className = config.buttonClassName;
   button.innerHTML = config.buttonLabel ? config.buttonLabel : 'Przeglądaj...';
   var fileInput = document.createElement('input');
   fileInput.type = 'file';
   fileInput.id = config.id ? config.id + '_input' : undefined;
   fileInput.style.fontSize = '100px';
   fileInput.style.position = 'absolute';
   fileInput.style.left = 0;
   fileInput.style.top = 0;
   fileInput.style.opacity = 0;
   fileInput.onchange = function() {
      if (config.onstart) config.onstart.call(this);
      var reader = new FileReader();
      var file = fileInput.files[0];
      reader.readAsText(file, 'UTF-8');
      if (config.accept) {
         var accepted;
         if (Array.isArray(config.accept)) {
            accepted = (config.accept.indexOf(file.type) >= 0);
         } else if (config.accept instanceof Function) {
            accepted = config.accept.call(this, file.type);
         }
         if (!accepted) {
            if (config.onwrongtype) config.onwrongtype.call(this, {
               name: file.name,
               type: file.type
            });
            return;
         }
      }
      reader.onprogress = function(evt) {
         if (config.onprogress) config.onprogress.call(this, {
            loaded: evt.loaded,
            total: evt.total
         });
      };
      reader.onload = function(evt) {
         if (config.onload) config.onload.call(this, {
            contents: evt.target.result
         });
      };
      reader.onerror = function(evt) {
         if (config.onerror) config.onerror.call(this);
      };
   };
   wrapper.appendChild(button);
   wrapper.appendChild(fileInput);
   return wrapper;
}

Teraz mo­że­my łat­wo ogra­ni­czyć moż­li­wość wczy­ty­wa­nia da­nych na przy­kład do pli­ków tek­sto­wych.

var uploadControl = new FileUploadControl({
   id: 'upload',
   buttonLabel: 'Otwórz...',
   accept: [ 'text/plain' ],
   onload: function(file) {
      document.getElementById('editor').innerHTML = file.contents;
   },
   onwrongtype: function(file) {
      alert('Niewłaściwy typ pliku. Wskaż inny plik i spróbuj ponownie.');
   },
   onerror: function(file) {
      alert('Błąd wczytywania zawartości pliku.');
   }
});
document.getElementById('form').appendChild(uploadControl);

Tak przy­go­to­wa­ny kom­po­nent mo­że być wy­ko­rzys­ty­wa­ny w pros­ty spo­sób. Wystar­czy uzu­peł­nić kon­struk­tor FileUploadControl do­ku­men­tac­ją Javadoc in­for­mu­ją­cą o zna­cze­niu po­szcze­gól­nych pól obiek­tu kon­fi­gu­ra­cyj­ne­go con­fig i przy­go­to­wać do­bre przy­kła­dy, go­to­we do wkle­je­nia w miej­scach, w któ­rych trze­ba użyć kom­po­nen­tu.

Warto też po­ku­sić się o roz­bu­do­wa­nie stwo­rzo­ne­go pro­to­ty­pu o me­to­dy bu­du­ją­ce, po­zwa­la­ją­ce usta­lać ce­chy kom­po­nen­tu bez ko­rzy­sta­nia z obiek­tu con­fig oraz zmie­niać je w trak­cie dzia­ła­nia apli­kac­ji.


Przedsta­wio­ne w do­tych­cza­so­wych dwóch arty­ku­łach przy­kła­dy do­ty­czą wczy­ty­wa­nia pli­ków tek­sto­wych. W więk­szoś­ci przy­pad­ków to ogra­ni­cze­nie jest cał­ko­wi­cie do za­ak­cep­to­wa­nia. Autorzy bar­dziej roz­bu­do­wa­nych apli­kac­ji, ta­kich jak edy­to­ry czcio­nek lub ikon, po­trze­bu­ją jed­nak moż­li­woś­ci od­czy­ty­wa­nia i in­ter­pre­to­wa­nia za­war­toś­ci pli­ków bi­nar­nych. O tym na­pi­szę w ko­lej­nym od­cin­ku te­go cyk­lu.