Skip to content

Instantly share code, notes, and snippets.

@bunyk
Last active November 1, 2016 23:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bunyk/5c93338529b6c3d4aeafc392840e4fea to your computer and use it in GitHub Desktop.
Save bunyk/5c93338529b6c3d4aeafc392840e4fea to your computer and use it in GitHub Desktop.
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<script type="text/javascript" src="lifelog.js"></script>
</head>
<body>
<script type="text/javascript">
var data = localStorage.lifelog;
var lifelog = Elm.LifeLog.fullscreen(
(data || null) && JSON.parse(data)
);
lifelog.ports.setStorage.subscribe(function(state) {
console.log('saving', state);
localStorage.lifelog = JSON.stringify(state);
});
</script>
</body>
</html>
-- Порти можна використовувати лише в порт-модулях, тому ми оголошуємо наш модуль порт-модулем
-- Таким чином ми не потрапимо в репозиторій пакетів ELm і не забруднимо його
-- Бруднокровкам заборонено лазити в репозиторії Elm. :)
-- Бруднокровність (те що ми порт) означає що ми спілкуватимемось з JavaScript,
-- що може викликати непередбачувані побічні ефекти. Чисті модулі в Elm працюють
-- лише згідно сигнатури.
port module LifeLog exposing (main)
-- Залежності
import Html exposing (body, h2, text, input, Html, Attribute, button, div, p, b, em)
import Html.App as App
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput, onClick, on, keyCode)
import Json.Decode as Json
import String exposing (length)
import Time exposing (Time, second)
-- programWithFlags означає що функція init приймає початкову модель ззовні
-- Тобто що коли ми скомпілюємо цей модуль і вставимо його в index.html,
-- ми зможемо запустити його передавши початкові дані:
-- var lifelog = Elm.LifeLog.fullscreen(initialData)
main =
App.programWithFlags
{ init = init -- як утворюється початкова модель
, view = view -- як модель перетворюється на те що бачить користувач
, update = update -- як модель змінюється у відповідь на різні події
, subscriptions = subscriptions -- які події окрім тих про які ми самі просимо можуть приходити
}
-- Вихідний порт
-- Виклик setStorage з моделлю поверне нам команду, яка викличе підписку на
-- стороні порта в JavaScript і передасть їй ті дані
-- lifelog.ports.setStorage.subscribe(function(state) {
-- localStorage.lifelog = JSON.stringify(state);
-- });
port setStorage : Model -> Cmd msg
-- MODEL
type alias LogEntry = { -- Складений тип "запис щоденника"
timestamp: Time, -- Час початку події
text: String, -- Опис події
duration: Time -- Тривалість події
}
type alias Model = { -- А це власне всі дані програми
now: Time, -- Поточний час
currentText: String, -- Поточний текст в редакторі
log: List LogEntry -- Список записів щоденника
}
emptyModel: Model -- Порожній щоденник
emptyModel = {
now = 0,
currentText = "",
log = []
}
-- Функція init приймає може модель (а може ніщо)
-- і повертає точно модель і початкову команду
-- Якщо приймає ніщо - повертає emptyModel, інакше ту модель яку передали
-- І за замовчуванням порожню команду
init : Maybe Model -> (Model, Cmd Msg)
init savedModel =
(Maybe.withDefault emptyModel savedModel, Cmd.none)
-- UPDATE
-- Повідомлення (події) на які програма може реагувати)
type Msg -- Це алгебраїчний тип (тобто може мати одне з наступних значень):
= Tick Time -- Оновити час на вказаний
| Edit String -- Замінити рядок на вказаний
| Submit -- Зробити новий запис в щоденник
| NoOp -- нічого не робити
-- Оновлення моделі
-- Приймає повідомлення і модель, повертає нову модель і команду для рушія Elm
-- Командою може бути запит на випадкове число, Http запит, спілкування з портом
-- і т.п.
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick newTime -> -- Якщо змінився час
(updateTime model newTime, Cmd.none) -- Оновити час в моделі і нічого не робити
Edit newText -> -- Якщо змінився текст
({model | currentText = newText}, Cmd.none) -- Оновити текст і нічого не робити
Submit -> -- Якщо ми хочемо додати новий запис
updateLog model -- То повернути нову модель і якісь команди
NoOp -> -- Якщо ми нічого не хочемо робити
(model, Cmd.none) -- То повертути ту ж модель і жодних команд
-- Оновлює модель і повертає команду передати її в порт setStorage
updateLog: Model -> (Model, Cmd Msg)
updateLog model =
let newModel =
{model | log = addEntry model, -- оновлюємо лог
currentText = "" -- очищаємо редактор для нового тексту
}
in
(newModel, setStorage newModel)
-- Додає новий запис до списку записів на основі поточної моделі
addEntry: Model -> List LogEntry
addEntry model =
let
newEntry = { -- Формуємо новий запис
text = model.currentText,
timestamp = model.now,
duration = 0
}
in
case model.log of
[] -> [newEntry] -- Якщо щоденник порожній - додати новий запис
-- Якщо ж вже містить хоч один елемент, то
-- Додати новий елемент перед ним
-- потім той що містився в кінці з оновленою тривалістю часу
-- а потім все решта без змін
le :: log -> newEntry :: updateDuration le model.now :: log
-- Отримати запис щоденника і поточний час, і оновити тривалість запису
updateDuration: LogEntry -> Time -> LogEntry
updateDuration entry new_time =
-- Тривалість - це поточний час мінус час початку
{entry | duration = new_time - entry.timestamp}
-- Функція яка бере модель і час, і повертає нову модель
-- Описує що відбувається якщо змінюється чаc
updateTime: Model -> Time -> Model
updateTime model time =
let
newLog = case model.log of -- Є два стани щоденника
[] -> [] -- порожній не змінюється
-- А в тому який містить хоч елемент змінюється тривалість останнього елемента.
head :: tail -> updateDuration head time :: tail
in
{model |
now = time, -- оновити поточний час в моделі
log = newLog -- і стан щоденника
}
-- SUBSCRIPTIONS
-- Функція повертає підписки (які повідомлення і в яких випадках Elm має генерувати)
-- В цьому випадку - щосекунди має приходити повідомлення Tick
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every second Tick
-- VIEW
-- Приймаємо модель і повертаємо HTML DOM та повідомлення (події які з того DOM
-- можуть приходити. Різні елементи можуть різні повідомлення відправляти)
view : Model -> Html Msg
view model =
body [] [ -- кожен елемент це функція що приймає список атрибутів і список піделементів
h2 [] [ text "Precise life log"],
list2html model.log,
text "It is ",
--- f <| g x - це те саме що f (g x) (<| - композиція функцій)
b [] [text <| formatTime model.now],
text " ",
input [
placeholder "What you are doing now?", -- приклад атрибуту
onInput Edit, -- приклад посилання повідомлення при події
value model.currentText,
onEnter Submit
] [],
button [onClick Submit] [text "So it goes"]
]
-- А це так робиться модульність у в'юшці
-- Приймаємо елемент щоденника, повертаємо частину DOM
item2html: LogEntry -> Html Msg
item2html l =
p [] [
b [] [text <| formatTime l.timestamp]
, text " "
,text l.text
, em [] [text (" Duration: (" ++ (formatTime l.duration) ++ ")")]
]
-- Приймаємо ввесь щоденник, повертаємо DOM що містить всі записи
list2html: List LogEntry -> Html Msg
list2html list =
div [] (List.map item2html (List.reverse list))
-- А так ми фільтруємо повідомлення що виходять з в'юшки
onEnter : Msg -> Attribute Msg
onEnter msg =
let
tagger code =
if code == 13 then msg else NoOp
in
on "keydown" (Json.map tagger keyCode)
-- TIME
-- TODO: replace with https://github.com/mgold/elm-date-format
getSeconds: Time -> Int
getSeconds time =
(floor <| Time.inSeconds time) % 60
getMinutes: Time -> Int
getMinutes time =
(floor <| Time.inMinutes time) % 60
getHours: Time -> Int
getHours time =
(floor <| Time.inHours time) % 24
addZero: String -> String
addZero s =
if (length s) >= 2 then s else "0" ++ s
formatTime: Time -> String
formatTime time =
(addZero <| toString <| getHours time)
++ ":" ++ (addZero <| toString <| getMinutes time)
++ ":" ++ (addZero <| toString <| getSeconds time)
default:
elm-make lifelog.elm --output=lifelog.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment