- {#if _is_uploading}
-
-
-
+ on:dragenter={dragenter}
+ on:dragleave={dragleave}
+ on:drop={drop}
+ class="kws-file-upload {klass} is-{color} is-{size}
+ is-{_error ? 'danger' : ''} {disabled ? 'is-disabled' : ''}
+ {_is_finished ? 'is-success' : ''} {_is_dragging ? 'is-dragging' : ''}">
+
+
+
+
+ {#if _is_uploading}
+
+ {:else if _is_finished}
+
+ {:else}
+
+ {/if}
+
+
+ {#if _is_uploading}
+
-
- {_progress}% - Uploading...
- {:else if _is_finished}
- Upload complete!
- {:else}
- {_filename}
- {/if}
-
-
-
-
- {#if _error}
-
-
{_error_message}
+
{_progress}% - Uploading...
+ {:else if _is_finished}
+
Upload complete!
+ {:else}
+
{_filename}
+ {/if}
+
+ {#if is_cloud_upload}
+
{:else}
-
-
- Max size: {maxFileSize}
-
-
-
- {info}
-
-
- {fileTypes}
-
+
{/if}
diff --git a/packages/@kws3/ui/controls/services/ajax.js b/packages/@kws3/ui/controls/services/ajax.js
new file mode 100644
index 000000000..232095d9f
--- /dev/null
+++ b/packages/@kws3/ui/controls/services/ajax.js
@@ -0,0 +1,130 @@
+// Best place to find information on XHR features is:
+// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
+
+var reqfields = ["responseType", "withCredentials", "timeout", "onprogress"];
+
+// Simple and small ajax function
+// Takes a parameters object and a callback function
+// Parameters:
+// - url: string, required
+// - headers: object of `{header_name: header_value, ...}`
+// - body:
+// + string (sets content type to 'application/x-www-form-urlencoded' if not set in headers)
+// + FormData (doesn't set content type so that browser will set as appropriate)
+// - method: 'GET', 'POST', etc. Defaults to 'GET' or 'POST' based on body
+// - cors: If your using cross-origin, you will need this true for IE8-9
+//
+// The following parameters are passed onto the xhr object.
+// IMPORTANT NOTE: The caller is responsible for compatibility checking.
+// - responseType: string, various compatability, see xhr docs for enum options
+// - withCredentials: boolean, IE10+, CORS only
+// - timeout: long, ms timeout, IE8+
+// - onprogress: callback, IE10+
+//
+// Callback function prototype:
+// - statusCode from request
+// + Possibly null or 0 (i.e. falsy) if an error occurs
+// - response
+// + if responseType set and supported by browser, this is an object of some type (see docs)
+// + otherwise if request completed, this is the string text of the response
+// + if request is aborted, this is "Abort"
+// + if request times out, this is "Timeout"
+// + if request errors before completing (probably a CORS issue), this is "Error"
+// - request object
+//
+// Returns the request object. So you can call .abort() or other methods
+//
+// DEPRECATIONS:
+// - Passing a string instead of the params object has been removed!
+//
+export default function (params, callback) {
+ // Any variable used more than once is var'd here because
+ // minification will munge the variables whereas it can't munge
+ // the object access.
+ var headers = params.headers || {},
+ body = params.body,
+ method = params.method || (body ? "POST" : "GET"),
+ called = false;
+
+ var req = getRequest(params.cors);
+
+ function cb(statusCode, responseText) {
+ return function () {
+ if (!called) {
+ callback(
+ req.status === undefined ? statusCode : req.status,
+ req.status === 0
+ ? "Error"
+ : req.response || req.responseText || responseText,
+ req
+ );
+ called = true;
+ }
+ };
+ }
+
+ req.open(method, params.url, true);
+
+ var success = (req.onload = cb(200));
+ req.onreadystatechange = function () {
+ if (req.readyState === 4) success();
+ };
+ req.onerror = cb(null, "Error");
+ req.ontimeout = cb(null, "Timeout");
+ req.onabort = cb(null, "Abort");
+
+ if (body) {
+ setDefault(headers, "X-Requested-With", "XMLHttpRequest");
+
+ if (window.FormData || window.Blob) {
+ if (
+ !(body instanceof window.FormData) &&
+ !(body instanceof window.Blob)
+ ) {
+ setDefault(
+ headers,
+ "Content-Type",
+ "application/x-www-form-urlencoded"
+ );
+ }
+ }
+ }
+
+ for (let i = 0, len = reqfields.length, field; i < len; i++) {
+ field = reqfields[i];
+ if (params[field] !== undefined) {
+ if (
+ body &&
+ ((window.FormData && body instanceof window.FormData) ||
+ (window.Blob && body instanceof window.Blob)) &&
+ field === "onprogress"
+ ) {
+ req.upload[field] = params[field];
+ } else {
+ req[field] = params[field];
+ }
+ }
+ }
+
+ for (let field in headers) req.setRequestHeader(field, headers[field]);
+
+ req.send(body);
+
+ return req;
+}
+
+function getRequest(cors) {
+ // XDomainRequest is only way to do CORS in IE 8 and 9
+ // But XDomainRequest isn't standards-compatible
+ // Notably, it doesn't allow cookies to be sent or set by servers
+ // IE 10+ is standards-compatible in its XMLHttpRequest
+ // but IE 10 can still have an XDomainRequest object, so we don't want to use it
+ if (cors && window.XDomainRequest && !/MSIE 1/.test(navigator.userAgent))
+ // eslint-disable-next-line no-undef
+ return new XDomainRequest();
+ if (window.XMLHttpRequest) return new XMLHttpRequest();
+}
+
+function setDefault(obj, key, value) {
+ obj[key] = obj[key] || value;
+}
diff --git a/packages/@kws3/ui/controls/services/net.js b/packages/@kws3/ui/controls/services/net.js
new file mode 100644
index 000000000..a82862acd
--- /dev/null
+++ b/packages/@kws3/ui/controls/services/net.js
@@ -0,0 +1,60 @@
+import EventEmitter from "microevent";
+import "promis";
+import Ajax from "./ajax";
+
+var network = function () {
+ var self = this,
+ key = { access_token: "", refresh_token: "", expires_in: null };
+
+ EventEmitter.mixin(self);
+
+ function send(params, isRaw, noApiKey) {
+ return new Promise(function (fulfil, reject) {
+ if (typeof params.headers == "undefined") {
+ params.headers = {};
+ }
+ if (!noApiKey) {
+ params.headers["Api-key"] = key.access_token;
+ }
+
+ Ajax(params, function (code, response, xhr) {
+ var resp = {};
+ if (code >= 200 && code < 400) {
+ if (isRaw) {
+ fulfil(response);
+ } else {
+ try {
+ resp = JSON.parse(response);
+ } catch (e) {
+ console.log(e);
+ }
+
+ if (typeof resp.records != "undefined") {
+ fulfil({
+ code: code,
+ response: resp.records,
+ xhr: xhr,
+ headers: xhr.getAllResponseHeaders(),
+ _meta: resp._meta,
+ });
+ } else {
+ //self.trigger("httpError", 501);
+ reject({ code: code, response: response });
+ }
+ }
+ }
+ });
+ });
+ }
+
+ self.raw = function (params, noApiKey) {
+ return send(params, true, noApiKey);
+ };
+
+ self.ajax = function (params) {
+ return send(params);
+ };
+};
+
+//this ensures a singleton of this class
+export default new network();
diff --git a/packages/@kws3/ui/controls/services/uploadQueueStore.js b/packages/@kws3/ui/controls/services/uploadQueueStore.js
new file mode 100644
index 000000000..b9792d41b
--- /dev/null
+++ b/packages/@kws3/ui/controls/services/uploadQueueStore.js
@@ -0,0 +1,138 @@
+import { derived, writable } from "svelte/store";
+
+const uploadQueueInit = () => {
+ const store = writable({}),
+ unsubList = {};
+
+ const create = (queue_name, newItem) => {
+ const __unique_id =
+ Math.random().toString(36).substring(2, 15) +
+ Math.random().toString(36).substring(2, 15);
+
+ store.update((items) => {
+ if (typeof items[queue_name] === "undefined") {
+ items[queue_name] = [];
+ }
+ items[queue_name].push({ __unique_id, queue_name, state: {} });
+ return items;
+ });
+
+ const unsub = newItem.subscribe((uploadState) => {
+ updateItem(queue_name, __unique_id, uploadState);
+ });
+
+ unsubList[__unique_id] = unsub;
+ };
+
+ const removeItem = (queue_name, __unique_id) => {
+ store.update((items) => {
+ if (typeof items[queue_name] !== "undefined") {
+ items[queue_name] = items[queue_name].filter((item) => {
+ let match = item.__unique_id !== __unique_id;
+ if (!match) {
+ //unsubscribe from readable store
+ unsubList[item.__unique_id] && unsubList[item.__unique_id]();
+ }
+ return match;
+ });
+ }
+
+ return items;
+ });
+ };
+
+ const clearCompleted = () => {
+ store.update((items) => {
+ for (let queue_name in items) {
+ items[queue_name] = items[queue_name].slice().filter((item) => {
+ if (item.state.status === "uploaded") {
+ //unsubscribe from readable store
+ unsubList[item.__unique_id] && unsubList[item.__unique_id]();
+ return false;
+ }
+ return true;
+ });
+ }
+ return items;
+ });
+ };
+
+ const updateItem = (queue_name, __unique_id, uploadState) => {
+ store.update((items) => {
+ if (typeof items[queue_name] !== "undefined") {
+ const _items = items[queue_name];
+ _items.slice().forEach((i) => {
+ if (i.__unique_id === __unique_id) {
+ i.state = uploadState;
+ }
+ });
+
+ items[queue_name] = _items;
+ }
+ return items;
+ });
+ };
+
+ return {
+ ...store,
+ create,
+ removeItem,
+ clearCompleted,
+ };
+};
+
+export const uploadQueue = uploadQueueInit();
+
+export const activeUploadsCount = derived(uploadQueue, (que) => {
+ let count = 0;
+ for (let queue_name in que) {
+ count += que[queue_name].length;
+ }
+ return count;
+});
+
+export const uploadsList = derived(uploadQueue, (que) => {
+ let uploads = [];
+ for (let queue_name in que) {
+ uploads = uploads.concat(que[queue_name]);
+ }
+ return uploads;
+});
+
+export const totalUploadProgress = derived(uploadsList, (ulist) => {
+ let total = 0,
+ active = 0,
+ complete = 0,
+ sizeTotal = 0,
+ failed = 0,
+ uploadedTotal = 0;
+
+ ulist.forEach((upload) => {
+ const status = upload.state.status;
+ total++;
+ if (status !== "uploaded" && status !== "error") {
+ active++;
+ }
+ if (status === "uploaded") {
+ complete++;
+ }
+ if (status === "failed") {
+ failed++;
+ }
+ sizeTotal += Number(upload.state.total);
+ uploadedTotal += Number(upload.state.loaded);
+ });
+
+ return {
+ total,
+ active,
+ complete,
+ failed,
+ remaining: total - complete,
+ progress: Math.round((uploadedTotal / sizeTotal) * 100),
+ };
+});
+
+export function registerUpload(queue_name, newItem) {
+ uploadQueue.create(queue_name, newItem);
+}
diff --git a/packages/@kws3/ui/controls/uploader/UploadNotification.svelte b/packages/@kws3/ui/controls/uploader/UploadNotification.svelte
new file mode 100644
index 000000000..e024f4422
--- /dev/null
+++ b/packages/@kws3/ui/controls/uploader/UploadNotification.svelte
@@ -0,0 +1,101 @@
+
+
+
+
+ {$activeUploadsCount} uploads
+
+
+ {#if canClose}
+
+
+
+ {/if}
+
+
+ {#if $totalUploadProgress.remaining}
+ {$totalUploadProgress.remaining} remaining {#if $totalUploadProgress.failed}({$totalUploadProgress.failed} failed){/if}
+ {:else}
+ All uploads complete
+ {/if}
+
+ Details
+
+
+ {#if showList}
+
+ {:else}
+
+ {/if}
+
+
+
diff --git a/packages/@kws3/ui/controls/uploader/UploadingItem.svelte b/packages/@kws3/ui/controls/uploader/UploadingItem.svelte
new file mode 100644
index 000000000..4b9ec44ba
--- /dev/null
+++ b/packages/@kws3/ui/controls/uploader/UploadingItem.svelte
@@ -0,0 +1,15 @@
+
+
{item.original_name}
+
+
+
+
diff --git a/packages/@kws3/ui/index.js b/packages/@kws3/ui/index.js
index 1ef6a8b11..e3ea82338 100644
--- a/packages/@kws3/ui/index.js
+++ b/packages/@kws3/ui/index.js
@@ -42,6 +42,7 @@ export { default as ProcessButton } from "./buttons/ProcessButton.svelte";
export { default as Checkbox } from "./controls/Checkbox.svelte";
export { default as FileUpload } from "./controls/FileUpload.svelte";
+export { default as CloudFileUpload } from "./controls/CloudFileUpload.svelte";
export { default as NumberInput } from "./controls/NumberInput.svelte";
export { default as Radio } from "./controls/Radio.svelte";
export { default as Toggle } from "./controls/Toggle.svelte";
diff --git a/packages/@kws3/ui/styles/FileUpload.scss b/packages/@kws3/ui/styles/FileUpload.scss
index cf17814c2..0a1a26b02 100644
--- a/packages/@kws3/ui/styles/FileUpload.scss
+++ b/packages/@kws3/ui/styles/FileUpload.scss
@@ -20,10 +20,9 @@ $kws-fileupload-progress-anim-easing: linear !default;
.file-upload-inner {
position: relative;
background: $kws-fileupload-bg-color;
- border: 1px solid $kws-fileupload-border-color;
border-radius: $kws-fileupload-radius;
max-width: 100%;
- height: 2.5rem;
+ min-height: 2.5rem;
.file {
overflow: hidden;
height: 100%;
diff --git a/packages/@kws3/ui/upload/index.js b/packages/@kws3/ui/upload/index.js
new file mode 100644
index 000000000..bed287d4f
--- /dev/null
+++ b/packages/@kws3/ui/upload/index.js
@@ -0,0 +1,60 @@
+const MIME_TYPES = {
+ png: {
+ type: "image/png",
+ icon: "file-image-o",
+ },
+ jpg: {
+ type: "image/jpg",
+ icon: "file-image-o",
+ },
+ jpeg: {
+ type: "image/jpeg",
+ icon: "file-image-o",
+ },
+ webp: {
+ type: "image/webp",
+ icon: "file-image-o",
+ },
+ pdf: {
+ type: "application/pdf",
+ icon: "file-pdf-o",
+ },
+ doc: {
+ type: "application/msword",
+ icon: "file-word-o",
+ },
+ docx: {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ icon: "file-word-o",
+ },
+ xls: {
+ type: "application/vnd.ms-excel",
+ icon: "file-excel-o",
+ },
+ xlsx: {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ icon: "file-excel-o",
+ },
+ ppt: {
+ type: "application/vnd.ms-powerpoint",
+ icon: "file-powerpoint-o",
+ },
+ pptx: {
+ type: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ icon: "file-powerpoint-o",
+ },
+};
+
+/**
+ * Get MIME.
+ * @param {array} [allowed_extension=[]] - Array of allowed extensions
+ */
+export function getMimesFor(allowed_extension = []) {
+ let accepted_mime = [];
+ allowed_extension.forEach((extension) => {
+ if (MIME_TYPES[extension]) {
+ accepted_mime.push(MIME_TYPES[extension].type);
+ }
+ });
+ return accepted_mime.join(",");
+}
diff --git a/src/stories/controls/FileUpload/CloudFileUploadDecorator.svelte b/src/stories/controls/FileUpload/CloudFileUploadDecorator.svelte
new file mode 100644
index 000000000..ccf812730
--- /dev/null
+++ b/src/stories/controls/FileUpload/CloudFileUploadDecorator.svelte
@@ -0,0 +1,44 @@
+
+
+
+ Cloud File Upload
+
+ prepareUpload(itemlId, data)}
+ acknowledger={(data) => ackUpload(itemlId, data)}
+ max={536870912}
+ label="Add image or document.."
+ allowed={allowed_ext}
+ accept={getMimesFor(allowed_ext)} />
+
+
+
+
diff --git a/src/stories/controls/FileUpload/FileUpload.svelte b/src/stories/controls/FileUpload/FileUpload.svelte
index 75fdfbe82..1d6a01b4b 100644
--- a/src/stories/controls/FileUpload/FileUpload.svelte
+++ b/src/stories/controls/FileUpload/FileUpload.svelte
@@ -1,5 +1,6 @@
+
Default design
onFileChosen(event, false)}
on:file_uploaded={() => onFileUploaded()}
@@ -13,26 +14,162 @@
class={klass}
{color}
{size}
- {info_color} />
- This will succeed.
+ {info_color}
+ {multiple}
+ {accept}
+ inner_style="min-height: 2.5rem;"
+ {is_cloud_upload} />
+
+ This will succeed.
+
+
+ Custom design-1
+
+
onFileChosen(event, false)}
+ on:file_uploaded={() => onFileUploaded()}
+ on:file_upload_error={() => onFileUploadError()}
+ {allowed}
+ {disabled}
+ message="Browse/drop file..."
+ {key}
+ {info}
+ {max}
+ class={klass}
+ {color}
+ {size}
+ {info_color}
+ {multiple}
+ {accept}
+ inner_style="min-height: 2.5rem;"
+ {is_cloud_upload}>
+
+
+ {#if uploading}
+
+ {:else if finished}
+
+ Finished
+
+ {:else}
+
+ {filename}
+
+
+ {/if}
+
+
+
+
+ This will succeed.
+
+
+
+
+
+ Custom design-2
+
onFileChosen(event, true)}
on:file_uploaded={() => onFileUploaded()}
on:file_upload_error={() => onFileUploadError()}
{allowed}
{disabled}
- {message}
+ message="Browse/Drop file(s)"
{key}
{info}
{max}
class={klass}
{color}
{size}
- {info_color} />
- This will fail.
+ {info_color}
+ {multiple}
+ {accept}
+ inner_style="min-height:8rem"
+ {is_cloud_upload}>
+
+
+
+
+
+ {#if uploading}
+
+ {:else if finished}
+
+ {:else}
+
+ {/if}
+
+ {#if uploading}
+
+ {progress ? progress + "% - " : ""} Uploading...
+
+ {:else if finished}
+
Upload complete!
+ {:else}
+
+ {filename}
+
+ {/if}
+
+
+
+
+ {#if error}
+
+ {error_message}
+
+ {:else}
+
+
+ {!multiple ? "Max size:" + maxFileSize : ""}
+
+
+ {/if}
+
+
+ {#if !error}
+
+ {fileTypes}
+
+ {/if}
+
+ {#if info}
+
+ {info}
+
+ {/if}
+
+
+
+
+
+
+ This will fail.
+
@@ -48,9 +185,63 @@